diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 4f3f1f4c..9beff306 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -136,16 +136,17 @@ jobs: declare -A additional_info additional_info["ibo"]="\n Discord: <@951737931159187457>\n AniList: [takarealist112]()" additional_info["aayush262"]="\n Discord: <@918825160654598224>\n AniList: [aayush262]()" - additional_info["rebelonion"]="\n Discord: <@714249925248024617>\n AniList: [rebelonion]()\n PornHub: [rebelonion]()" + additional_info["rebel onion"]="\n Discord: <@714249925248024617>\n AniList: [rebelonion]()\n PornHub: [rebelonion]()" additional_info["Ankit Grai"]="\n Discord: <@1125628254330560623>\n AniList: [bheshnarayan]()" # Decimal color codes for contributors declare -A contributor_colors - default_color="16721401" - contributor_colors["grayankit"]="#350297" + default_color="#bf2cc8" contributor_colors["ibo"]="#ff9b46" contributor_colors["aayush262"]="#5d689d" contributor_colors["Sadwhy"]="#ff7e95" + contributor_colors["grayankit"]="#c51aa1" + contributor_colors["rebelonion"]="#d4e5ed" hex_to_decimal() { printf '%d' "0x${1#"#"}"; } @@ -179,7 +180,7 @@ jobs: top_contributor="" top_contributor_count=0 top_contributor_avatar="" - embed_color=$default_color + embed_color=$(hex_to_decimal "$default_color") # Process contributors in the new order while read -r login; do @@ -201,7 +202,7 @@ jobs: elif [ $commit_count -eq $max_commits ]; then top_contributors+=("$login") top_contributor_count=$((top_contributor_count + 1)) - embed_color=$default_color + embed_color=$(hex_to_decimal "$default_color") fi echo "Debug top contributors:" echo "$top_contributors" @@ -241,7 +242,7 @@ jobs: thumbnail_url="$top_contributor_avatar" else thumbnail_url="https://i.imgur.com/5o3Y9Jb.gif" - embed_color=$default_color + embed_color=$(hex_to_decimal "$default_color") fi # Truncate field values diff --git a/app/build.gradle b/app/build.gradle index 5d422b29..915b3ba8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { minSdk 21 targetSdk 35 versionCode((System.currentTimeMillis() / 60000).toInteger()) - versionName "3.1.0" - versionCode 300100000 + versionName "3.2.0" + versionCode 300200000 signingConfig signingConfigs.debug } diff --git a/app/src/google/java/ani/dantotsu/others/AppUpdater.kt b/app/src/google/java/ani/dantotsu/others/AppUpdater.kt index f6f8e8ca..36b4c35d 100644 --- a/app/src/google/java/ani/dantotsu/others/AppUpdater.kt +++ b/app/src/google/java/ani/dantotsu/others/AppUpdater.kt @@ -29,7 +29,6 @@ import ani.dantotsu.util.Logger import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch -import kotlinx.coroutines.time.delay import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonArray diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 644d218a..fe6f1a90 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -394,11 +394,10 @@ - + - - + PrefName.MangaExtensionRepos to "Manga" + "aniyomi" -> PrefName.AnimeExtensionRepos to "Anime" + "novelyomi" -> PrefName.NovelExtensionRepos to "Novel" + else -> throw Exception("Invalid scheme") } val savedRepos: Set = PrefManager.getVal(prefName) val newRepos = savedRepos.toMutableSet() AddRepositoryBottomSheet.addRepoWarning(this) { newRepos.add(url) PrefManager.setVal(prefName, newRepos) - toast("${if (uri.scheme == "tachiyomi") "Manga" else "Anime"} Extension Repo added") + toast("$name Extension Repo added") } return } @@ -488,9 +489,9 @@ class MainActivity : AppCompatActivity() { return@passwordAlertDialog } if (PreferencePackager.unpack(decryptedJson)) { - val intent = Intent(this, this.javaClass) + val newIntent = Intent(this, this.javaClass) this.finish() - startActivity(intent) + startActivity(newIntent) } } else { toast("Password cannot be empty") @@ -499,9 +500,9 @@ class MainActivity : AppCompatActivity() { } else if (name.endsWith(".ani")) { val decryptedJson = jsonString.toString(Charsets.UTF_8) if (PreferencePackager.unpack(decryptedJson)) { - val intent = Intent(this, this.javaClass) + val newIntent = Intent(this, this.javaClass) this.finish() - startActivity(intent) + startActivity(newIntent) } } else { toast("Invalid file type") diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AniMangaSearchResults.kt similarity index 70% rename from app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt rename to app/src/main/java/ani/dantotsu/connections/anilist/AniMangaSearchResults.kt index 6c2ca504..e8ef1a39 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AniMangaSearchResults.kt @@ -2,15 +2,25 @@ package ani.dantotsu.connections.anilist import ani.dantotsu.R import ani.dantotsu.currContext +import ani.dantotsu.media.Author +import ani.dantotsu.media.Character import ani.dantotsu.media.Media +import ani.dantotsu.media.Studio +import ani.dantotsu.profile.User import java.io.Serializable -data class SearchResults( +interface SearchResults { + var search: String? + var page: Int + var results: MutableList + var hasNextPage: Boolean +} + +data class AniMangaSearchResults( val type: String, var isAdult: Boolean, var onList: Boolean? = null, var perPage: Int? = null, - var search: String? = null, var countryOfOrigin: String? = null, var sort: String? = null, var genres: MutableList? = null, @@ -23,10 +33,11 @@ data class SearchResults( var seasonYear: Int? = null, var startYear: Int? = null, var season: String? = null, - var page: Int = 1, - var results: MutableList, - var hasNextPage: Boolean, -) : Serializable { + override var search: String? = null, + override var page: Int = 1, + override var results: MutableList, + override var hasNextPage: Boolean, +) : SearchResults, Serializable { fun toChipList(): List { val list = mutableListOf() sort?.let { @@ -108,4 +119,33 @@ data class SearchResults( val type: String, val text: String ) -} \ No newline at end of file +} + +data class CharacterSearchResults( + override var search: String?, + override var page: Int = 1, + override var results: MutableList, + override var hasNextPage: Boolean, +) : SearchResults, Serializable + +data class StudioSearchResults( + override var search: String?, + override var page: Int = 1, + override var results: MutableList, + override var hasNextPage: Boolean, +) : SearchResults, Serializable + + +data class StaffSearchResults( + override var search: String?, + override var page: Int = 1, + override var results: MutableList, + override var hasNextPage: Boolean, +) : SearchResults, Serializable + +data class UserSearchResults( + override var search: String?, + override var page: Int = 1, + override var results: MutableList, + override var hasNextPage: Boolean, +) : SearchResults, Serializable \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt b/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt index 6acb67a4..02d7fc14 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt @@ -311,7 +311,6 @@ object Anilist { ) val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1 Logger.log("Remaining requests: $remaining") - println("Remaining requests: $remaining") if (json.code == 429) { val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1 val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0 diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt index 4c1ececd..237ef453 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt @@ -44,7 +44,8 @@ class AnilistQueries { val response: Query.Viewer? measureTimeMillis { response = executeQuery( - """{Viewer{name options{timezone titleLanguage staffNameLanguage activityMergeTime airingNotifications displayAdultContent restrictMessagesToFollowing} avatar{medium} bannerImage id mediaListOptions{scoreFormat rowOrder animeList{customLists} mangaList{customLists}} statistics{anime{episodesWatched} manga{chaptersRead}} unreadNotificationCount}}""") + """{Viewer{name options{timezone titleLanguage staffNameLanguage activityMergeTime airingNotifications displayAdultContent restrictMessagesToFollowing} avatar{medium} bannerImage id mediaListOptions{scoreFormat rowOrder animeList{customLists} mangaList{customLists}} statistics{anime{episodesWatched} manga{chaptersRead}} unreadNotificationCount}}""" + ) }.also { println("time : $it") } val user = response?.data?.user ?: return false @@ -96,12 +97,10 @@ class AnilistQueries { fun mediaDetails(media: Media): Media { media.cameFromContinue = false - - val query = - """{Media(id:${media.id}){id favourites popularity episodes chapters streamingEpisodes {title thumbnail url site} mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}reviews(perPage:3, sort:SCORE_DESC){nodes{id mediaId mediaType summary body(asHtml:true) rating ratingAmount userRating score private siteUrl createdAt updatedAt user{id name bannerImage avatar{medium large}}}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}""" runBlocking { val anilist = async { - var response = executeQuery(query, force = true) + var response = + executeQuery(fullMediaInformation(media.id), force = true) if (response != null) { fun parse() { val fetchedMedia = response?.data?.media ?: return @@ -291,7 +290,10 @@ class AnilistQueries { val firstStudio = get(0) media.anime.mainStudio = Studio( firstStudio.id.toString(), - firstStudio.name ?: "N/A" + firstStudio.name ?: "N/A", + firstStudio.isFavourite ?: false, + firstStudio.favourites ?: 0, + null ) } } @@ -333,7 +335,11 @@ class AnilistQueries { if (response.data?.media != null) parse() else { snackString(currContext()?.getString(R.string.adult_stuff)) - response = executeQuery(query, force = true, useToken = false) + response = executeQuery( + fullMediaInformation(media.id), + force = true, + useToken = false + ) if (response?.data?.media != null) parse() else snackString(currContext()?.getString(R.string.what_did_you_open)) } @@ -400,8 +406,6 @@ class AnilistQueries { return media } - - private suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList { var hasNextPage = true var page = 0 @@ -426,8 +430,6 @@ class AnilistQueries { return responseArray } - - suspend fun getUserStatus(): ArrayList? { val toShow: List = PrefManager.getVal(PrefName.HomeLayout) @@ -485,19 +487,23 @@ class AnilistQueries { return list.toCollection(ArrayList()) } else return null } + private fun favMediaQuery(anime: Boolean, page: Int, id: Int? = Anilist.userid): String { - return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}""" + return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){$standardPageInformation edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}""" } + private fun recommendationQuery(): String { - return """ Page(page: 1, perPage:30) { pageInfo { total currentPage hasNextPage } recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } """ + return """ Page(page: 1, perPage:30) { $standardPageInformation recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } """ } private fun recommendationPlannedQuery(type: String): String { return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING${if (type == "ANIME") ", sort: MEDIA_POPULARITY_DESC" else ""} ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }""" } + private fun continueMediaQuery(type: String, status: String): String { return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } """ } + suspend fun initHomePage(): Map> { val removeList = PrefManager.getCustomVal("removeList", setOf()) val hidePrivate = PrefManager.getVal(PrefName.HidePrivate) @@ -888,7 +894,7 @@ class AnilistQueries { return null } - suspend fun search( + suspend fun searchAniManga( type: String, page: Int? = null, perPage: Int? = null, @@ -910,52 +916,7 @@ class AnilistQueries { id: Int? = null, hd: Boolean = false, adultOnly: Boolean = false - ): SearchResults? { - val query = """ -query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) { - Page(page: ${"$"}page, perPage: ${perPage ?: 50}) { - pageInfo { - total - perPage - currentPage - lastPage - hasNextPage - } - media(id: ${"$"}id, type: ${"$"}type, season: ${"$"}season, format_in: ${"$"}format, status: ${"$"}status, countryOfOrigin: ${"$"}countryOfOrigin, source: ${"$"}source, search: ${"$"}search, onList: ${"$"}onList, seasonYear: ${"$"}seasonYear, startDate_like: ${"$"}year, startDate_lesser: ${"$"}yearLesser, startDate_greater: ${"$"}yearGreater, episodes_lesser: ${"$"}episodeLesser, episodes_greater: ${"$"}episodeGreater, duration_lesser: ${"$"}durationLesser, duration_greater: ${"$"}durationGreater, chapters_lesser: ${"$"}chapterLesser, chapters_greater: ${"$"}chapterGreater, volumes_lesser: ${"$"}volumeLesser, volumes_greater: ${"$"}volumeGreater, licensedBy_in: ${"$"}licensedBy, isLicensed: ${"$"}isLicensed, genre_in: ${"$"}genres, genre_not_in: ${"$"}excludedGenres, tag_in: ${"$"}tags, tag_not_in: ${"$"}excludedTags, minimumTagRank: ${"$"}minimumTagRank, sort: ${"$"}sort, isAdult: ${"$"}isAdult) { - id - idMal - isAdult - status - chapters - episodes - nextAiringEpisode { - episode - } - type - genres - meanScore - isFavourite - format - bannerImage - coverImage { - large - extraLarge - } - title { - english - romaji - userPreferred - } - mediaListEntry { - progress - private - score(format: POINT_100) - status - } - } - } -} - """.replace("\n", " ").replace(""" """, "") + ): AniMangaSearchResults? { val variables = """{"type":"$type","isAdult":$isAdult ${if (adultOnly) ""","isAdult":true""" else ""} ${if (onList != null) ""","onList":$onList""" else ""} @@ -1000,8 +961,9 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: }]""" else "" } - }""".replace("\n", " ").replace(""" """, "") - val response = executeQuery(query, variables, true)?.data?.page + }""".prepare() + val response = + executeQuery(aniMangaSearch(perPage), variables, true)?.data?.page if (response?.media != null) { val responseArray = arrayListOf() response.media?.forEach { i -> @@ -1021,7 +983,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: val pageInfo = response.pageInfo ?: return null - return SearchResults( + return AniMangaSearchResults( type = type, perPage = perPage, search = search, @@ -1047,6 +1009,169 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: return null } + suspend fun searchCharacters(page: Int, search: String?): CharacterSearchResults? { + if (search.isNullOrBlank()) return null + val query = """ + { + Page(page: $page, perPage: $ITEMS_PER_PAGE) { + $standardPageInformation + characters(search: "$search") { + ${characterInformation(false)} + } + } + } + """.prepare() + + val response = executeQuery(query, force = true)?.data?.page + + if (response?.characters != null) { + val responseArray = arrayListOf() + response.characters?.forEach { i -> + responseArray.add( + Character( + i.id, + i.name?.full, + i.image?.medium ?: i.image?.large, + null, + null.toString(), + i.isFavourite ?: false, + i.description, + i.age, + i.gender, + i.dateOfBirth, + ) + ) + } + + val pageInfo = response.pageInfo ?: return null + + return CharacterSearchResults( + search = search, + results = responseArray, + page = pageInfo.currentPage ?: 0, + hasNextPage = pageInfo.hasNextPage == true + ) + } + return null + } + + suspend fun searchStudios(page: Int, search: String?): StudioSearchResults? { + if (search.isNullOrBlank()) return null + val query = """ + { + Page(page: $page, perPage: $ITEMS_PER_PAGE) { + $standardPageInformation + studios(search: "$search") { + ${studioInformation(1, 1)} + } + } + } + """.prepare() + + val response = executeQuery(query, force = true)?.data?.page + + if (response?.studios != null) { + val responseArray = arrayListOf() + response.studios?.forEach { i -> + responseArray.add( + Studio( + i.id.toString(), + i.name ?: return null, + i.isFavourite ?: false, + i.favourites, + i.media?.edges?.firstOrNull()?.node?.let { it.coverImage?.large } + ) + ) + } + + val pageInfo = response.pageInfo ?: return null + + return StudioSearchResults( + search = search, + results = responseArray, + page = pageInfo.currentPage ?: 0, + hasNextPage = pageInfo.hasNextPage == true + ) + } + return null + } + + suspend fun searchStaff(page: Int, search: String?): StaffSearchResults? { + if (search.isNullOrBlank()) return null + val query = """ + { + Page(page: $page, perPage: $ITEMS_PER_PAGE) { + $standardPageInformation + staff(search: "$search") { + ${staffInformation(1, 1)} + } + } + } + """.prepare() + + val response = executeQuery(query, force = true)?.data?.page + + if (response?.staff != null) { + val responseArray = arrayListOf() + response.staff?.forEach { i -> + responseArray.add( + Author( + i.id, + i.name?.userPreferred ?: return null, + i.image?.large, + null, + null, + null + ) + ) + } + + val pageInfo = response.pageInfo ?: return null + + return StaffSearchResults( + search = search, + results = responseArray, + page = pageInfo.currentPage ?: 0, + hasNextPage = pageInfo.hasNextPage == true + ) + } + return null + } + + suspend fun searchUsers(page: Int, search: String?): UserSearchResults? { + val query = """ + { + Page(page: $page, perPage: $ITEMS_PER_PAGE) { + $standardPageInformation + users(search: "$search") { + ${userInformation()} + } + } + } + """.prepare() + + val response = executeQuery(query, force = true)?.data?.page + + if (response?.users != null) { + val users = response.users?.map { user -> + User( + user.id, + user.name ?: return null, + user.avatar?.medium, + user.bannerImage + ) + } ?: return null + + return UserSearchResults( + search = search, + results = users.toMutableList(), + page = response.pageInfo?.currentPage ?: 0, + hasNextPage = response.pageInfo?.hasNextPage == true + ) + } + return null + } + private fun mediaList(media1: Page?): ArrayList { val combinedList = arrayListOf() media1?.media?.mapTo(combinedList) { Media(it) } @@ -1071,26 +1196,65 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: val countryFilter = country?.let { "countryOfOrigin:$it, " } ?: "" return buildString { - append("""Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:$sort, type:$type, $formatFilter $countryFilter $includeList $isAdult){id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}""") + append("""Page(page:1,perPage:50){$standardPageInformation media(sort:$sort, type:$type, $formatFilter $countryFilter $includeList $isAdult){id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}""") } } private fun recentAnimeUpdates(page: Int): String { val currentTime = System.currentTimeMillis() / 1000 return buildString { - append("""Page(page:$page,perPage:50){pageInfo{hasNextPage total}airingSchedules(airingAt_greater:0 airingAt_lesser:${currentTime - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}""") + append("""Page(page:$page,perPage:50){$standardPageInformation airingSchedules(airingAt_greater:0 airingAt_lesser:${currentTime - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}""") } } private fun queryAnimeList(): String { return buildString { - append("""{recentUpdates:${recentAnimeUpdates(1)} recentUpdates2:${recentAnimeUpdates(2)} trendingMovies:${buildQueryString("POPULARITY_DESC", "ANIME", "MOVIE")} topRated:${buildQueryString("SCORE_DESC", "ANIME")} mostFav:${buildQueryString("FAVOURITES_DESC", "ANIME")}}""") + append( + """{recentUpdates:${recentAnimeUpdates(1)} recentUpdates2:${recentAnimeUpdates(2)} trendingMovies:${ + buildQueryString( + "POPULARITY_DESC", + "ANIME", + "MOVIE" + ) + } topRated:${ + buildQueryString( + "SCORE_DESC", + "ANIME" + ) + } mostFav:${buildQueryString("FAVOURITES_DESC", "ANIME")}}""" + ) } } private fun queryMangaList(): String { return buildString { - append("""{trendingManga:${buildQueryString("POPULARITY_DESC", "MANGA", country = "JP")} trendingManhwa:${buildQueryString("POPULARITY_DESC", "MANGA", country = "KR")} trendingNovel:${buildQueryString("POPULARITY_DESC", "MANGA", format = "NOVEL", country = "JP")} topRated:${buildQueryString("SCORE_DESC", "MANGA")} mostFav:${buildQueryString("FAVOURITES_DESC", "MANGA")}}""") + append( + """{trendingManga:${ + buildQueryString( + "POPULARITY_DESC", + "MANGA", + country = "JP" + ) + } trendingManhwa:${ + buildQueryString( + "POPULARITY_DESC", + "MANGA", + country = "KR" + ) + } trendingNovel:${ + buildQueryString( + "POPULARITY_DESC", + "MANGA", + format = "NOVEL", + country = "JP" + ) + } topRated:${ + buildQueryString( + "SCORE_DESC", + "MANGA" + ) + } mostFav:${buildQueryString("FAVOURITES_DESC", "MANGA")}}""" + ) } } @@ -1145,7 +1309,6 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: list } - suspend fun recentlyUpdated( greater: Long = 0, lesser: Long = System.currentTimeMillis() / 1000 - 10000 @@ -1153,10 +1316,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: suspend fun execute(page: Int = 1): Page? { val query = """{ Page(page:$page,perPage:50) { - pageInfo { - hasNextPage - total - } + $standardPageInformation airingSchedules( airingAt_greater: $greater airingAt_lesser: $lesser @@ -1165,35 +1325,11 @@ Page(page:$page,perPage:50) { episode airingAt media { - id - idMal - status - chapters - episodes - nextAiringEpisode { episode } - isAdult - type - meanScore - isFavourite - format - bannerImage - countryOfOrigin - coverImage { large } - title { - english - romaji - userPreferred - } - mediaListEntry { - progress - private - score(format: POINT_100) - status - } + ${standardMediaInformation()} } } } - }""".replace("\n", " ").replace(""" """, "") + }""".prepare() return executeQuery(query, force = true)?.data?.page } @@ -1221,68 +1357,37 @@ Page(page:$page,perPage:50) { suspend fun getCharacterDetails(character: Character): Character { val query = """ { Character(id: ${character.id}) { - id - age - gender - description - dateOfBirth { - year - month - day - } - media(page: 0,sort:[POPULARITY_DESC,SCORE_DESC]) { - pageInfo { - total - perPage - currentPage - lastPage - hasNextPage - } - edges { - id - characterRole - node { - id - idMal - isAdult - status - chapters - episodes - nextAiringEpisode { episode } - type - meanScore - isFavourite - format - bannerImage - countryOfOrigin - coverImage { large } - title { - english - romaji - userPreferred - } - mediaListEntry { - progress - private - score(format: POINT_100) - status - } - } - } - } + ${characterInformation(true)} } -}""".replace("\n", " ").replace(""" """, "") - executeQuery(query, force = true)?.data?.character?.apply { - character.age = age - character.gender = gender - character.description = description - character.dateOfBirth = dateOfBirth - character.roles = arrayListOf() - media?.edges?.forEach { i -> - val m = Media(i) - m.relation = i.characterRole.toString() - character.roles?.add(m) - } +}""".prepare() + executeQuery(query, force = true)?.data?.character?.let { i -> + return Character( + i.id, + i.name?.full, + i.image?.large ?: i.image?.medium, + null, + null.toString(), + i.isFavourite ?: false, + i.description, + i.age, + i.gender, + i.dateOfBirth, + i.media?.edges?.map { + val m = Media(it) + m.relation = it.characterRole.toString() + m + }?.let { ArrayList(it) }, + i.media?.edges?.flatMap { edge -> + edge.voiceActors?.map { va -> + Author( + va.id, + va.name?.userPreferred, + va.image?.large ?: va.image?.medium, + va.languageV2 + ) + } ?: emptyList() + } as ArrayList? + ) } return character } @@ -1290,45 +1395,9 @@ Page(page:$page,perPage:50) { suspend fun getStudioDetails(studio: Studio): Studio { fun query(page: Int = 0) = """ { Studio(id: ${studio.id}) { - id - media(page: $page,sort:START_DATE_DESC) { - pageInfo{ - hasNextPage - } - edges { - id - node { - id - idMal - isAdult - status - chapters - episodes - nextAiringEpisode { episode } - type - meanScore - startDate{ year } - isFavourite - format - bannerImage - countryOfOrigin - coverImage { large } - title { - english - romaji - userPreferred - } - mediaListEntry { - progress - private - score(format: POINT_100) - status - } - } - } - } + ${studioInformation(page, ITEMS_PER_PAGE)} } -}""".replace("\n", " ").replace(""" """, "") +}""".prepare() var hasNextPage = true val yearMedia = mutableMapOf>() @@ -1363,66 +1432,15 @@ Page(page:$page,perPage:50) { suspend fun getAuthorDetails(author: Author): Author { fun query(page: Int = 0) = """ { Staff(id: ${author.id}) { - id - staffMedia(page: $page,sort:START_DATE_DESC) { - pageInfo{ - hasNextPage - } - edges { - staffRole - id - node { - id - idMal - isAdult - status - chapters - episodes - nextAiringEpisode { episode } - type - meanScore - startDate{ year } - isFavourite - format - bannerImage - countryOfOrigin - coverImage { large } - title { - english - romaji - userPreferred - } - mediaListEntry { - progress - private - score(format: POINT_100) - status - } - } - } - } + ${staffInformation(page, ITEMS_PER_PAGE)} characters(page: $page,sort:FAVOURITES_DESC) { - pageInfo{ - hasNextPage - } + $standardPageInformation nodes{ - id - name { - first - middle - last - full - native - userPreferred - } - image { - large - medium - } + ${characterInformation(false)} } } } -}""".replace("\n", " ").replace(""" """, "") +}""".prepare() var hasNextPage = true val yearMedia = mutableMapOf>() @@ -1433,6 +1451,16 @@ Page(page:$page,perPage:50) { val query = executeQuery( query(page), force = true )?.data?.author + author.age = query?.age + author.yearsActive = + if (query?.yearsActive?.isEmpty() == true) null else query?.yearsActive + author.homeTown = if (query?.homeTown?.isBlank() == true) null else query?.homeTown + author.dateOfDeath = if (query?.dateOfDeath?.toStringOrEmpty() + ?.isBlank() == true + ) null else query?.dateOfDeath?.toStringOrEmpty() + author.dateOfBirth = if (query?.dateOfBirth?.toStringOrEmpty() + ?.isBlank() == true + ) null else query?.dateOfBirth?.toStringOrEmpty() hasNextPage = query?.staffMedia?.let { it.edges?.forEach { i -> i.node?.apply { @@ -1480,14 +1508,14 @@ Page(page:$page,perPage:50) { sort: String = "SCORE_DESC" ): Query.ReviewsResponse? { return executeQuery( - """{Page(page:$page,perPage:10){pageInfo{currentPage,hasNextPage,total}reviews(mediaId:$mediaId,sort:$sort){id,mediaId,mediaType,summary,body(asHtml:true)rating,ratingAmount,userRating,score,private,siteUrl,createdAt,updatedAt,user{id,name,bannerImage avatar{medium,large}}}}}""", + """{Page(page:$page,perPage:10){$standardPageInformation reviews(mediaId:$mediaId,sort:$sort){id,mediaId,mediaType,summary,body(asHtml:true)rating,ratingAmount,userRating,score,private,siteUrl,createdAt,updatedAt,user{id,name,bannerImage avatar{medium,large}}}}}""", force = true ) } suspend fun getUserProfile(id: Int): Query.UserProfileResponse? { return executeQuery( - """{followerPage:Page{followers(userId:$id){id}pageInfo{total}}followingPage:Page{following(userId:$id){id}pageInfo{total}}user:User(id:$id){id name about(asHtml:true)avatar{medium large}bannerImage isFollowing isFollower isBlocked favourites{anime{nodes{id coverImage{extraLarge large medium color}}}manga{nodes{id coverImage{extraLarge large medium color}}}characters{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}staff{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}studios{nodes{id name isFavourite}}}statistics{anime{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}manga{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}}siteUrl}}""", + """{followerPage:Page{followers(userId:$id){id}$standardPageInformation}followingPage:Page{following(userId:$id){id}$standardPageInformation}user:User(id:$id){id name about(asHtml:true)avatar{medium large}bannerImage isFollowing isFollower isBlocked favourites{anime{nodes{id coverImage{extraLarge large medium color}}}manga{nodes{id coverImage{extraLarge large medium color}}}characters{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}staff{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}studios{nodes{id name isFavourite}}}statistics{anime{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}manga{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}}siteUrl}}""", force = true ) } @@ -1513,7 +1541,7 @@ Page(page:$page,perPage:50) { } private fun userFavMediaQuery(anime: Boolean, id: Int): String { - return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:1){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}""" + return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:1){$standardPageInformation edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}""" } suspend fun userFollowing(id: Int): Query.Following? { @@ -1535,7 +1563,7 @@ Page(page:$page,perPage:50) { """{ favoriteAnime:${userFavMediaQuery(true, id)} favoriteManga:${userFavMediaQuery(false, id)} - }""".trimIndent(), force = true + }""".prepare(), force = true ) } @@ -1549,7 +1577,7 @@ Page(page:$page,perPage:50) { val typeIn = "type_in:[AIRING,MEDIA_MERGE,MEDIA_DELETION,MEDIA_DATA_CHANGE]" val reset = if (resetNotification) "true" else "false" val res = executeQuery( - """{User(id:$id){unreadNotificationCount}Page(page:$page,perPage:$ITEMS_PER_PAGE){pageInfo{currentPage,hasNextPage}notifications(resetNotificationCount:$reset , ${if (type == true) typeIn else ""}){__typename...on AiringNotification{id,type,animeId,episode,contexts,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}},}...on FollowingNotification{id,userId,type,context,createdAt,user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMessageNotification{id,userId,type,activityId,context,createdAt,message{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMentionNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplySubscribedNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentMentionNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentReplyNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentSubscribedNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentLikeNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadLikeNotification{id,userId,type,threadId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on RelatedMediaAdditionNotification{id,type,context,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDataChangeNotification{id,type,mediaId,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaMergeNotification{id,type,mediaId,deletedMediaTitles,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDeletionNotification{id,type,deletedMediaTitle,context,reason,createdAt,}}}}""", + """{User(id:$id){unreadNotificationCount}Page(page:$page,perPage:$ITEMS_PER_PAGE){$standardPageInformation notifications(resetNotificationCount:$reset , ${if (type == true) typeIn else ""}){__typename...on AiringNotification{id,type,animeId,episode,contexts,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}},}...on FollowingNotification{id,userId,type,context,createdAt,user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMessageNotification{id,userId,type,activityId,context,createdAt,message{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMentionNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplySubscribedNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentMentionNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentReplyNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentSubscribedNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentLikeNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadLikeNotification{id,userId,type,threadId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on RelatedMediaAdditionNotification{id,type,context,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDataChangeNotification{id,type,mediaId,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaMergeNotification{id,type,mediaId,deletedMediaTitles,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDeletionNotification{id,type,deletedMediaTitle,context,reason,createdAt,}}}}""", force = true ) if (res != null && resetNotification) { @@ -1571,7 +1599,8 @@ Page(page:$page,perPage:50) { else if (userId != null) "userId:$userId," else if (global) "isFollowing:false,hasRepliesOrTypeText:true," else "isFollowing:true," - val typeIn = if (filter == "isFollowing:true,") "type_in:[TEXT,ANIME_LIST,MANGA_LIST,MEDIA_LIST]," else "" + val typeIn = + if (filter == "isFollowing:true,") "type_in:[TEXT,ANIME_LIST,MANGA_LIST,MEDIA_LIST]," else "" return executeQuery( """{Page(page:$page,perPage:$ITEMS_PER_PAGE){activities(${filter}${typeIn}sort:ID_DESC){__typename ... on TextActivity{id userId type replyCount text(asHtml:true)siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}... on ListActivity{id userId type replyCount status progress siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}media{id title{english romaji native userPreferred}bannerImage coverImage{medium large}isAdult}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}... on MessageActivity{id recipientId messengerId type replyCount likeCount message(asHtml:true)isLocked isSubscribed isLiked isPrivate siteUrl createdAt recipient{id name bannerImage avatar{medium large}}messenger{id name bannerImage avatar{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}}}}""", force = true diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt index ee978214..572f69ed 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt @@ -128,7 +128,7 @@ class AnilistHomeViewModel : ViewModel() { class AnilistAnimeViewModel : ViewModel() { var searched = false var notSet = true - lateinit var searchResults: SearchResults + lateinit var aniMangaSearchResults: AniMangaSearchResults private val type = "ANIME" private val trending: MutableLiveData> = MutableLiveData>(null) @@ -137,7 +137,7 @@ class AnilistAnimeViewModel : ViewModel() { suspend fun loadTrending(i: Int) { val (season, year) = Anilist.currentSeasons[i] trending.postValue( - Anilist.query.search( + Anilist.query.searchAniManga( type, perPage = 12, sort = Anilist.sortBy[2], @@ -150,9 +150,9 @@ class AnilistAnimeViewModel : ViewModel() { } - private val animePopular = MutableLiveData(null) + private val animePopular = MutableLiveData(null) - fun getPopular(): LiveData = animePopular + fun getPopular(): LiveData = animePopular suspend fun loadPopular( type: String, searchVal: String? = null, @@ -161,7 +161,7 @@ class AnilistAnimeViewModel : ViewModel() { onList: Boolean = true, ) { animePopular.postValue( - Anilist.query.search( + Anilist.query.searchAniManga( type, search = searchVal, onList = if (onList) null else false, @@ -173,8 +173,8 @@ class AnilistAnimeViewModel : ViewModel() { } - suspend fun loadNextPage(r: SearchResults) = animePopular.postValue( - Anilist.query.search( + suspend fun loadNextPage(r: AniMangaSearchResults) = animePopular.postValue( + Anilist.query.searchAniManga( r.type, r.page + 1, r.perPage, @@ -224,7 +224,7 @@ class AnilistAnimeViewModel : ViewModel() { class AnilistMangaViewModel : ViewModel() { var searched = false var notSet = true - lateinit var searchResults: SearchResults + lateinit var aniMangaSearchResults: AniMangaSearchResults private val type = "MANGA" private val trending: MutableLiveData> = MutableLiveData>(null) @@ -232,7 +232,7 @@ class AnilistMangaViewModel : ViewModel() { fun getTrending(): LiveData> = trending suspend fun loadTrending() = trending.postValue( - Anilist.query.search( + Anilist.query.searchAniManga( type, perPage = 10, sort = Anilist.sortBy[2], @@ -242,8 +242,8 @@ class AnilistMangaViewModel : ViewModel() { ) - private val mangaPopular = MutableLiveData(null) - fun getPopular(): LiveData = mangaPopular + private val mangaPopular = MutableLiveData(null) + fun getPopular(): LiveData = mangaPopular suspend fun loadPopular( type: String, searchVal: String? = null, @@ -252,7 +252,7 @@ class AnilistMangaViewModel : ViewModel() { onList: Boolean = true, ) { mangaPopular.postValue( - Anilist.query.search( + Anilist.query.searchAniManga( type, search = searchVal, onList = if (onList) null else false, @@ -264,8 +264,8 @@ class AnilistMangaViewModel : ViewModel() { } - suspend fun loadNextPage(r: SearchResults) = mangaPopular.postValue( - Anilist.query.search( + suspend fun loadNextPage(r: AniMangaSearchResults) = mangaPopular.postValue( + Anilist.query.searchAniManga( r.type, r.page + 1, r.perPage, @@ -325,14 +325,126 @@ class AnilistMangaViewModel : ViewModel() { } class AnilistSearch : ViewModel() { + + enum class SearchType { + ANIME, MANGA, CHARACTER, STAFF, STUDIO, USER; + + companion object { + + fun SearchType.toAnilistString(): String { + return when (this) { + ANIME -> "ANIME" + MANGA -> "MANGA" + CHARACTER -> "CHARACTER" + STAFF -> "STAFF" + STUDIO -> "STUDIO" + USER -> "USER" + } + } + + fun fromString(string: String): SearchType { + return when (string.uppercase()) { + "ANIME" -> ANIME + "MANGA" -> MANGA + "CHARACTER" -> CHARACTER + "STAFF" -> STAFF + "STUDIO" -> STUDIO + "USER" -> USER + else -> throw IllegalArgumentException("Invalid search type") + } + } + } + } + var searched = false var notSet = true - lateinit var searchResults: SearchResults - private val result: MutableLiveData = MutableLiveData(null) + lateinit var aniMangaSearchResults: AniMangaSearchResults + private val aniMangaResult: MutableLiveData = MutableLiveData(null) - fun getSearch(): LiveData = result - suspend fun loadSearch(r: SearchResults) = result.postValue( - Anilist.query.search( + lateinit var characterSearchResults: CharacterSearchResults + private val characterResult: MutableLiveData = MutableLiveData(null) + + lateinit var studioSearchResults: StudioSearchResults + private val studioResult: MutableLiveData = MutableLiveData(null) + + lateinit var staffSearchResults: StaffSearchResults + private val staffResult: MutableLiveData = MutableLiveData(null) + + lateinit var userSearchResults: UserSearchResults + private val userResult: MutableLiveData = MutableLiveData(null) + + fun getSearch(type: SearchType): MutableLiveData { + return when (type) { + SearchType.ANIME, SearchType.MANGA -> aniMangaResult as MutableLiveData + SearchType.CHARACTER -> characterResult as MutableLiveData + SearchType.STUDIO -> studioResult as MutableLiveData + SearchType.STAFF -> staffResult as MutableLiveData + SearchType.USER -> userResult as MutableLiveData + } + } + + suspend fun loadSearch(type: SearchType) { + when (type) { + SearchType.ANIME, SearchType.MANGA -> loadAniMangaSearch(aniMangaSearchResults) + SearchType.CHARACTER -> loadCharacterSearch(characterSearchResults) + SearchType.STUDIO -> loadStudiosSearch(studioSearchResults) + SearchType.STAFF -> loadStaffSearch(staffSearchResults) + SearchType.USER -> loadUserSearch(userSearchResults) + } + } + + suspend fun loadNextPage(type: SearchType) { + when (type) { + SearchType.ANIME, SearchType.MANGA -> loadNextAniMangaPage(aniMangaSearchResults) + SearchType.CHARACTER -> loadNextCharacterPage(characterSearchResults) + SearchType.STUDIO -> loadNextStudiosPage(studioSearchResults) + SearchType.STAFF -> loadNextStaffPage(staffSearchResults) + SearchType.USER -> loadNextUserPage(userSearchResults) + } + } + + fun hasNextPage(type: SearchType): Boolean { + return when (type) { + SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.hasNextPage + SearchType.CHARACTER -> characterSearchResults.hasNextPage + SearchType.STUDIO -> studioSearchResults.hasNextPage + SearchType.STAFF -> staffSearchResults.hasNextPage + SearchType.USER -> userSearchResults.hasNextPage + } + } + + fun resultsIsNotEmpty(type: SearchType): Boolean { + return when (type) { + SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.results.isNotEmpty() + SearchType.CHARACTER -> characterSearchResults.results.isNotEmpty() + SearchType.STUDIO -> studioSearchResults.results.isNotEmpty() + SearchType.STAFF -> staffSearchResults.results.isNotEmpty() + SearchType.USER -> userSearchResults.results.isNotEmpty() + } + } + + fun size(type: SearchType): Int { + return when (type) { + SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.results.size + SearchType.CHARACTER -> characterSearchResults.results.size + SearchType.STUDIO -> studioSearchResults.results.size + SearchType.STAFF -> staffSearchResults.results.size + SearchType.USER -> userSearchResults.results.size + } + } + + fun clearResults(type: SearchType) { + when (type) { + SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.results.clear() + SearchType.CHARACTER -> characterSearchResults.results.clear() + SearchType.STUDIO -> studioSearchResults.results.clear() + SearchType.STAFF -> staffSearchResults.results.clear() + SearchType.USER -> userSearchResults.results.clear() + } + } + + private suspend fun loadAniMangaSearch(r: AniMangaSearchResults) = aniMangaResult.postValue( + Anilist.query.searchAniManga( r.type, r.page, r.perPage, @@ -354,8 +466,36 @@ class AnilistSearch : ViewModel() { ) ) - suspend fun loadNextPage(r: SearchResults) = result.postValue( - Anilist.query.search( + private suspend fun loadCharacterSearch(r: CharacterSearchResults) = characterResult.postValue( + Anilist.query.searchCharacters( + r.page, + r.search, + ) + ) + + private suspend fun loadStudiosSearch(r: StudioSearchResults) = studioResult.postValue( + Anilist.query.searchStudios( + r.page, + r.search, + ) + ) + + private suspend fun loadStaffSearch(r: StaffSearchResults) = staffResult.postValue( + Anilist.query.searchStaff( + r.page, + r.search, + ) + ) + + private suspend fun loadUserSearch(r: UserSearchResults) = userResult.postValue( + Anilist.query.searchUsers( + r.page, + r.search, + ) + ) + + private suspend fun loadNextAniMangaPage(r: AniMangaSearchResults) = aniMangaResult.postValue( + Anilist.query.searchAniManga( r.type, r.page + 1, r.perPage, @@ -376,6 +516,34 @@ class AnilistSearch : ViewModel() { r.season ) ) + + private suspend fun loadNextCharacterPage(r: CharacterSearchResults) = characterResult.postValue( + Anilist.query.searchCharacters( + r.page + 1, + r.search, + ) + ) + + private suspend fun loadNextStudiosPage(r: StudioSearchResults) = studioResult.postValue( + Anilist.query.searchStudios( + r.page + 1, + r.search, + ) + ) + + private suspend fun loadNextStaffPage(r: StaffSearchResults) = staffResult.postValue( + Anilist.query.searchStaff( + r.page + 1, + r.search, + ) + ) + + private suspend fun loadNextUserPage(r: UserSearchResults) = userResult.postValue( + Anilist.query.searchUsers( + r.page + 1, + r.search, + ) + ) } class GenresViewModel : ViewModel() { diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/anilistGraphql.kt b/app/src/main/java/ani/dantotsu/connections/anilist/anilistGraphql.kt new file mode 100644 index 00000000..2d288168 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/connections/anilist/anilistGraphql.kt @@ -0,0 +1,429 @@ +package ani.dantotsu.connections.anilist + +val standardPageInformation = """ + pageInfo { + total + perPage + currentPage + lastPage + hasNextPage + } +""".prepare() + +fun String.prepare() = this.trimIndent().replace("\n", " ").replace(""" """, "") + +fun characterInformation(includeMediaInfo: Boolean) = """ + id + name { + first + middle + last + full + native + userPreferred + } + image { + large + medium + } + age + gender + description + dateOfBirth { + year + month + day + } + ${if (includeMediaInfo) """ + media(page: 0,sort:[POPULARITY_DESC,SCORE_DESC]) { + $standardPageInformation + edges { + id + voiceActors { + id, + name { + userPreferred + } + languageV2, + image { + medium, + large + } + } + characterRole + node { + id + idMal + isAdult + status + chapters + episodes + nextAiringEpisode { episode } + type + meanScore + isFavourite + format + bannerImage + countryOfOrigin + coverImage { large } + title { + english + romaji + userPreferred + } + mediaListEntry { + progress + private + score(format: POINT_100) + status + } + } + } + }""".prepare() else ""} +""".prepare() + +fun studioInformation(page: Int, perPage: Int) = """ + id + name + isFavourite + favourites + media(page: $page, sort:START_DATE_DESC, perPage: $perPage) { + $standardPageInformation + edges { + id + node { + id + idMal + isAdult + status + chapters + episodes + nextAiringEpisode { episode } + type + meanScore + startDate{ year } + isFavourite + format + bannerImage + countryOfOrigin + coverImage { large } + title { + english + romaji + userPreferred + } + mediaListEntry { + progress + private + score(format: POINT_100) + status + } + } + } + } +""".prepare() + +fun staffInformation(page: Int, perPage: Int) = """ + id + name { + first + middle + last + full + native + userPreferred + } + image { + large + medium + } + dateOfBirth { + year + month + day + } + dateOfDeath { + year + month + day + } + age + yearsActive + homeTown + staffMedia(page: $page,sort:START_DATE_DESC, perPage: $perPage) { + $standardPageInformation + edges { + staffRole + id + node { + id + idMal + isAdult + status + chapters + episodes + nextAiringEpisode { episode } + type + meanScore + startDate{ year } + isFavourite + format + bannerImage + countryOfOrigin + coverImage { large } + title { + english + romaji + userPreferred + } + mediaListEntry { + progress + private + score(format: POINT_100) + status + } + } + } + } +""".prepare() + +fun userInformation() = """ + id + name + about(asHtml: true) + avatar { + large + medium + } + bannerImage + isFollowing + isFollower + isBlocked + siteUrl +""".prepare() + +fun aniMangaSearch(perPage: Int?) = """ + query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) { + Page(page: ${"$"}page, perPage: ${perPage ?: 50}) { + $standardPageInformation + media(id: ${"$"}id, type: ${"$"}type, season: ${"$"}season, format_in: ${"$"}format, status: ${"$"}status, countryOfOrigin: ${"$"}countryOfOrigin, source: ${"$"}source, search: ${"$"}search, onList: ${"$"}onList, seasonYear: ${"$"}seasonYear, startDate_like: ${"$"}year, startDate_lesser: ${"$"}yearLesser, startDate_greater: ${"$"}yearGreater, episodes_lesser: ${"$"}episodeLesser, episodes_greater: ${"$"}episodeGreater, duration_lesser: ${"$"}durationLesser, duration_greater: ${"$"}durationGreater, chapters_lesser: ${"$"}chapterLesser, chapters_greater: ${"$"}chapterGreater, volumes_lesser: ${"$"}volumeLesser, volumes_greater: ${"$"}volumeGreater, licensedBy_in: ${"$"}licensedBy, isLicensed: ${"$"}isLicensed, genre_in: ${"$"}genres, genre_not_in: ${"$"}excludedGenres, tag_in: ${"$"}tags, tag_not_in: ${"$"}excludedTags, minimumTagRank: ${"$"}minimumTagRank, sort: ${"$"}sort, isAdult: ${"$"}isAdult) { + ${standardMediaInformation()} + } + } + } +""".prepare() + +fun standardMediaInformation() = """ +id +idMal +siteUrl +isAdult +status(version: 2) +chapters +episodes +nextAiringEpisode { + episode + airingAt +} +type +genres +meanScore +popularity +favourites +isFavourite +format +bannerImage +countryOfOrigin +coverImage { + large + extraLarge +} +title { + english + romaji + userPreferred +} +mediaListEntry { + progress + private + score(format: POINT_100) + status +} +""".prepare() + +fun fullMediaInformation(id: Int) = """ +{ + Media(id: $id) { + streamingEpisodes { + title + thumbnail + url + site + } + mediaListEntry { + id + status + score(format: POINT_100) + progress + private + notes + repeat + customLists + updatedAt + startedAt { + year + month + day + } + completedAt { + year + month + day + } + } + reviews(perPage: 3, sort: SCORE_DESC) { + nodes { + id + mediaId + mediaType + summary + body(asHtml: true) + rating + ratingAmount + userRating + score + private + siteUrl + createdAt + updatedAt + user { + id + name + bannerImage + avatar { + medium + large + } + } + } + } + ${standardMediaInformation()} + source + duration + season + seasonYear + startDate { + year + month + day + } + endDate { + year + month + day + } + studios(isMain: true) { + nodes { + id + name + siteUrl + } + } + description + trailer { + site + id + } + synonyms + tags { + name + rank + isMediaSpoiler + } + characters(sort: [ROLE, FAVOURITES_DESC], perPage: 25, page: 1) { + edges { + role + voiceActors { + id + name { + first + middle + last + full + native + userPreferred + } + image { + large + medium + } + languageV2 + } + node { + id + image { + medium + } + name { + userPreferred + } + isFavourite + } + } + } + relations { + edges { + relationType(version: 2) + node { + ${standardMediaInformation()} + } + } + } + staffPreview: staff(perPage: 8, sort: [RELEVANCE, ID]) { + edges { + role + node { + id + image { + large + medium + } + name { + userPreferred + } + } + } + } + recommendations(sort: RATING_DESC) { + nodes { + mediaRecommendation { + ${standardMediaInformation()} + } + } + } + externalLinks { + url + site + } + } + Page(page: 1) { + $standardPageInformation + mediaList(isFollowing: true, sort: [STATUS], mediaId: $id) { + id + status + score(format: POINT_100) + progress + progressVolumes + user { + id + name + avatar { + large + medium + } + } + } + } +} + +""".prepare() \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt index a54e3ad8..b833ae8e 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt @@ -446,7 +446,7 @@ data class MediaEdge( @SerialName("staffRole") var staffRole: String?, // The voice actors of the character - // @SerialName("voiceActors") var voiceActors: List?, + @SerialName("voiceActors") var voiceActors: List?, // The voice actors of the character with role date // @SerialName("voiceActorRoles") var voiceActorRoles: List?, diff --git a/app/src/main/java/ani/dantotsu/download/DownloadCompat.kt b/app/src/main/java/ani/dantotsu/download/DownloadCompat.kt index bd4e5c84..831a7e73 100644 --- a/app/src/main/java/ani/dantotsu/download/DownloadCompat.kt +++ b/app/src/main/java/ani/dantotsu/download/DownloadCompat.kt @@ -260,7 +260,7 @@ class DownloadCompat { "$mangaLink/${it.name}", it.name, null, - null, + "Unknown", SChapter.create() ) chapters.add(chapter) diff --git a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt index 771684db..34c78db3 100644 --- a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt +++ b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt @@ -60,7 +60,7 @@ class DownloadsManager(private val context: Context) { onFinished: () -> Unit ) { removeDownloadCompat(context, downloadedType, toast) - downloadsList.remove(downloadedType) + downloadsList.removeAll { it.titleName == downloadedType.titleName && it.chapterName == downloadedType.chapterName } CoroutineScope(Dispatchers.IO).launch { removeDirectory(downloadedType, toast) withContext(Dispatchers.Main) { @@ -234,7 +234,7 @@ class DownloadsManager(private val context: Context) { val directory = baseDirectory?.findFolder(downloadedType.titleName) ?.findFolder(downloadedType.chapterName) - downloadsList.remove(downloadedType) + downloadsList.removeAll { it.titleName == downloadedType.titleName && it.chapterName == downloadedType.chapterName } // Check if the directory exists and delete it recursively if (directory?.exists() == true) { val deleted = directory.deleteRecursively(context, false) @@ -401,10 +401,13 @@ data class DownloadedType( @Deprecated("use pTitle instead") private val title: String? = null, @Deprecated("use pChapter instead") - private val chapter: String? = null + private val chapter: String? = null, + val scanlator: String = "Unknown" ) : Serializable { val titleName: String get() = title ?: pTitle.findValidName() val chapterName: String get() = chapter ?: pChapter.findValidName() + val uniqueName: String + get() = "$chapterName-${scanlator}" } diff --git a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt index ed3cb02c..58e7de38 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt @@ -243,7 +243,7 @@ class MangaDownloaderService : Service() { builder.setProgress(task.imageData.size, farthest, false) broadcastDownloadProgress( - task.chapter, + task.uniqueName, farthest * 100 / task.imageData.size ) if (notifi) { @@ -270,17 +270,18 @@ class MangaDownloaderService : Service() { DownloadedType( task.title, task.chapter, - MediaType.MANGA + MediaType.MANGA, + scanlator = task.scanlator, ) ) - broadcastDownloadFinished(task.chapter) + broadcastDownloadFinished(task.uniqueName) snackString("${task.title} - ${task.chapter} Download finished") } } catch (e: Exception) { Logger.log("Exception while downloading file: ${e.message}") snackString("Exception while downloading file: ${e.message}") Injekt.get().logException(e) - broadcastDownloadFailed(task.chapter) + broadcastDownloadFailed(task.uniqueName) } } @@ -423,11 +424,15 @@ class MangaDownloaderService : Service() { data class DownloadTask( val title: String, val chapter: String, + val scanlator: String, val imageData: List, val sourceMedia: Media? = null, val retries: Int = 2, val simultaneousDownloads: Int = 2, - ) + ) { + val uniqueName: String + get() = "$chapter-$scanlator" + } companion object { private const val NOTIFICATION_ID = 1103 diff --git a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt index 87713885..f11b8b4a 100644 --- a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt +++ b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt @@ -24,7 +24,7 @@ import ani.dantotsu.Refresh import ani.dantotsu.bottomBar import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.AnilistAnimeViewModel -import ani.dantotsu.connections.anilist.SearchResults +import ani.dantotsu.connections.anilist.AniMangaSearchResults import ani.dantotsu.connections.anilist.getUserId import ani.dantotsu.databinding.FragmentAnimeBinding import ani.dantotsu.media.MediaAdaptor @@ -100,7 +100,7 @@ class AnimeFragment : Fragment() { var loading = true if (model.notSet) { model.notSet = false - model.searchResults = SearchResults( + model.aniMangaSearchResults = AniMangaSearchResults( "ANIME", isAdult = false, onList = false, @@ -109,7 +109,7 @@ class AnimeFragment : Fragment() { sort = Anilist.sortBy[1] ) } - val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity()) + val popularAdaptor = MediaAdaptor(1, model.aniMangaSearchResults.results, requireActivity()) val progressAdaptor = ProgressAdapter(searched = model.searched) val adapter = ConcatAdapter(animePageAdapter, popularAdaptor, progressAdaptor) binding.animePageRecyclerView.adapter = adapter @@ -142,7 +142,7 @@ class AnimeFragment : Fragment() { animePageAdapter.onIncludeListClick = { checked -> oldIncludeList = !checked loading = true - model.searchResults.results.clear() + model.aniMangaSearchResults.results.clear() popularAdaptor.notifyDataSetChanged() scope.launch(Dispatchers.IO) { model.loadPopular("ANIME", sort = Anilist.sortBy[1], onList = checked) @@ -152,17 +152,17 @@ class AnimeFragment : Fragment() { model.getPopular().observe(viewLifecycleOwner) { if (it != null) { if (oldIncludeList == (it.onList != false)) { - val prev = model.searchResults.results.size - model.searchResults.results.addAll(it.results) + val prev = model.aniMangaSearchResults.results.size + model.aniMangaSearchResults.results.addAll(it.results) popularAdaptor.notifyItemRangeInserted(prev, it.results.size) } else { - model.searchResults.results.addAll(it.results) + model.aniMangaSearchResults.results.addAll(it.results) popularAdaptor.notifyDataSetChanged() oldIncludeList = it.onList ?: true } - model.searchResults.onList = it.onList - model.searchResults.hasNextPage = it.hasNextPage - model.searchResults.page = it.page + model.aniMangaSearchResults.onList = it.onList + model.aniMangaSearchResults.hasNextPage = it.hasNextPage + model.aniMangaSearchResults.page = it.page if (it.hasNextPage) progressAdaptor.bar?.visibility = View.VISIBLE else { @@ -177,10 +177,10 @@ class AnimeFragment : Fragment() { RecyclerView.OnScrollListener() { override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) { if (!v.canScrollVertically(1)) { - if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) { + if (model.aniMangaSearchResults.hasNextPage && model.aniMangaSearchResults.results.isNotEmpty() && !loading) { scope.launch(Dispatchers.IO) { loading = true - model.loadNextPage(model.searchResults) + model.loadNextPage(model.aniMangaSearchResults) } } } diff --git a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt index 0f31fca2..428489a6 100644 --- a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt @@ -13,7 +13,6 @@ import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -21,7 +20,7 @@ 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.connections.anilist.AnilistSearch.SearchType.Companion.toAnilistString import ani.dantotsu.databinding.ItemAnimePageBinding import ani.dantotsu.databinding.LayoutTrendingBinding import ani.dantotsu.getAppString @@ -83,13 +82,21 @@ class AnimePageAdapter : RecyclerView.Adapter oldIncludeList = !checked loading = true - model.searchResults.results.clear() + model.aniMangaSearchResults.results.clear() popularAdaptor.notifyDataSetChanged() scope.launch(Dispatchers.IO) { model.loadPopular("MANGA", sort = Anilist.sortBy[1], onList = checked) @@ -230,17 +230,17 @@ class MangaFragment : Fragment() { model.getPopular().observe(viewLifecycleOwner) { if (it != null) { if (oldIncludeList == (it.onList != false)) { - val prev = model.searchResults.results.size - model.searchResults.results.addAll(it.results) + val prev = model.aniMangaSearchResults.results.size + model.aniMangaSearchResults.results.addAll(it.results) popularAdaptor.notifyItemRangeInserted(prev, it.results.size) } else { - model.searchResults.results.addAll(it.results) + model.aniMangaSearchResults.results.addAll(it.results) popularAdaptor.notifyDataSetChanged() oldIncludeList = it.onList ?: true } - model.searchResults.onList = it.onList - model.searchResults.hasNextPage = it.hasNextPage - model.searchResults.page = it.page + model.aniMangaSearchResults.onList = it.onList + model.aniMangaSearchResults.hasNextPage = it.hasNextPage + model.aniMangaSearchResults.page = it.page if (it.hasNextPage) progressAdaptor.bar?.visibility = View.VISIBLE else { diff --git a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt index 2577e3b2..55e46c34 100644 --- a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt @@ -82,13 +82,21 @@ class MangaPageAdapter : RecyclerView.Adapter 0 && PrefManager.getVal(PrefName.ShowNotificationRedDot) == true trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString() - trendingBinding.searchBar.hint = "MANGA" + trendingBinding.searchBar.hint = binding.root.context.getString(R.string.search) trendingBinding.searchBarText.setOnClickListener { - ContextCompat.startActivity( - it.context, - Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"), - null - ) + val context = binding.root.context + if (PrefManager.getVal(PrefName.AniMangaSearchDirect) && Anilist.token != null) { + ContextCompat.startActivity( + context, + Intent(context, SearchActivity::class.java).putExtra("type", "MANGA"), + null + ) + } else { + SearchBottomSheet.newInstance().show( + (context as AppCompatActivity).supportFragmentManager, + "search" + ) + } } trendingBinding.userAvatar.setSafeOnClickListener { diff --git a/app/src/main/java/ani/dantotsu/home/SearchBottomSheet.kt b/app/src/main/java/ani/dantotsu/home/SearchBottomSheet.kt new file mode 100644 index 00000000..9eadae0d --- /dev/null +++ b/app/src/main/java/ani/dantotsu/home/SearchBottomSheet.kt @@ -0,0 +1,74 @@ +package ani.dantotsu.home + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import ani.dantotsu.BottomSheetDialogFragment +import ani.dantotsu.connections.anilist.AnilistSearch.SearchType +import ani.dantotsu.connections.anilist.AnilistSearch.SearchType.Companion.toAnilistString +import ani.dantotsu.databinding.BottomSheetSearchBinding +import ani.dantotsu.media.SearchActivity + +class SearchBottomSheet : BottomSheetDialogFragment() { + private var _binding: BottomSheetSearchBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = BottomSheetSearchBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.animeSearch.setOnClickListener { + startActivity(requireContext(), SearchType.ANIME) + dismiss() + } + binding.mangaSearch.setOnClickListener { + startActivity(requireContext(), SearchType.MANGA) + dismiss() + } + binding.characterSearch.setOnClickListener { + startActivity(requireContext(), SearchType.CHARACTER) + dismiss() + } + binding.staffSearch.setOnClickListener { + startActivity(requireContext(), SearchType.STAFF) + dismiss() + } + binding.studioSearch.setOnClickListener { + startActivity(requireContext(), SearchType.STUDIO) + dismiss() + } + binding.userSearch.setOnClickListener { + startActivity(requireContext(), SearchType.USER) + dismiss() + } + } + + private fun startActivity(context: Context, type: SearchType) { + ContextCompat.startActivity( + context, + Intent(context, SearchActivity::class.java).putExtra("type", type.toAnilistString()), + null + ) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + fun newInstance() = SearchBottomSheet() + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt b/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt index ced8b2d8..972cf4bb 100644 --- a/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt +++ b/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt @@ -9,14 +9,13 @@ import androidx.core.view.updateLayoutParams import ani.dantotsu.R import ani.dantotsu.connections.anilist.api.Activity import ani.dantotsu.databinding.ActivityStatusBinding -import ani.dantotsu.initActivity -import ani.dantotsu.themes.ThemeManager import ani.dantotsu.home.status.listener.StoriesCallback +import ani.dantotsu.initActivity import ani.dantotsu.navBarHeight import ani.dantotsu.profile.User import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.statusBarHeight -import ani.dantotsu.toast +import ani.dantotsu.themes.ThemeManager import ani.dantotsu.util.Logger class StatusActivity : AppCompatActivity(), StoriesCallback { diff --git a/app/src/main/java/ani/dantotsu/media/Author.kt b/app/src/main/java/ani/dantotsu/media/Author.kt index 803ef989..46baf07d 100644 --- a/app/src/main/java/ani/dantotsu/media/Author.kt +++ b/app/src/main/java/ani/dantotsu/media/Author.kt @@ -7,6 +7,12 @@ data class Author( var name: String?, var image: String?, var role: String?, + var age: Int? = null, + var yearsActive: List? = null, + var dateOfBirth: String? = null, + var dateOfDeath: String? = null, + var homeTown: String? = null, var yearMedia: MutableMap>? = null, - var character: ArrayList? = null + var character: ArrayList? = null, + var isFav: Boolean = false ) : Serializable diff --git a/app/src/main/java/ani/dantotsu/media/AuthorActivity.kt b/app/src/main/java/ani/dantotsu/media/AuthorActivity.kt index e30d6049..b3ecde2d 100644 --- a/app/src/main/java/ani/dantotsu/media/AuthorActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/AuthorActivity.kt @@ -1,11 +1,13 @@ package ani.dantotsu.media +import android.content.Intent import android.os.Bundle import android.view.View import android.view.ViewGroup import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat +import androidx.core.math.MathUtils.clamp import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.lifecycle.MutableLiveData @@ -16,57 +18,127 @@ import androidx.recyclerview.widget.LinearLayoutManager import ani.dantotsu.EmptyAdapter import ani.dantotsu.R import ani.dantotsu.Refresh -import ani.dantotsu.databinding.ActivityAuthorBinding +import ani.dantotsu.connections.anilist.Anilist +import ani.dantotsu.connections.anilist.AnilistMutations +import ani.dantotsu.databinding.ActivityCharacterBinding import ani.dantotsu.initActivity +import ani.dantotsu.loadImage import ani.dantotsu.navBarHeight +import ani.dantotsu.openLinkInBrowser +import ani.dantotsu.others.ImageViewDialog +import ani.dantotsu.others.SpoilerPlugin import ani.dantotsu.others.getSerialized import ani.dantotsu.px +import ani.dantotsu.settings.saving.PrefManager +import ani.dantotsu.settings.saving.PrefName +import ani.dantotsu.snackString import ani.dantotsu.statusBarHeight import ani.dantotsu.themes.ThemeManager +import com.google.android.material.appbar.AppBarLayout +import io.noties.markwon.Markwon +import io.noties.markwon.SoftBreakAddsNewLinePlugin import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.math.abs -class AuthorActivity : AppCompatActivity() { - private lateinit var binding: ActivityAuthorBinding +class AuthorActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener { + private lateinit var binding: ActivityCharacterBinding private val scope = lifecycleScope private val model: OtherDetailsViewModel by viewModels() - private var author: Author? = null + private lateinit var author: Author private var loaded = false + + private var screenWidth: Float = 0f + private val percent = 30 + private var mMaxScrollSize = 0 + private var isCollapsed = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ThemeManager(this).applyTheme() - binding = ActivityAuthorBinding.inflate(layoutInflater) + binding = ActivityCharacterBinding.inflate(layoutInflater) setContentView(binding.root) initActivity(this) - this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg) + screenWidth = resources.displayMetrics.run { widthPixels / density } + if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor = + ContextCompat.getColor(this, R.color.transparent) - val screenWidth = resources.displayMetrics.run { widthPixels / density } + val banner = + if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen - binding.root.updateLayoutParams { topMargin += statusBarHeight } - binding.studioRecycler.updatePadding(bottom = 64f.px + navBarHeight) - binding.studioTitle.isSelected = true + banner.updateLayoutParams { height += statusBarHeight } + binding.characterClose.updateLayoutParams { topMargin += statusBarHeight } + binding.characterCollapsing.minimumHeight = statusBarHeight + binding.characterCover.updateLayoutParams { topMargin += statusBarHeight } + binding.characterRecyclerView.updatePadding(bottom = 64f.px + navBarHeight) + binding.characterTitle.isSelected = true + binding.characterAppBar.addOnOffsetChangedListener(this) - author = intent.getSerialized("author") - binding.studioTitle.text = author?.name - - binding.studioClose.setOnClickListener { + binding.characterClose.setOnClickListener { onBackPressedDispatcher.onBackPressed() } + + author = intent.getSerialized("author") ?: return + binding.characterTitle.text = author.name + binding.characterCoverImage.loadImage(author.image) + binding.characterCoverImage.setOnLongClickListener { + ImageViewDialog.newInstance( + this, + author.name, + author.image + ) + } + val link = "https://anilist.co/staff/${author.id}" + binding.characterShare.setOnClickListener { + val i = Intent(Intent.ACTION_SEND) + i.type = "text/plain" + i.putExtra(Intent.EXTRA_TEXT, link) + startActivity(Intent.createChooser(i, author.name)) + } + binding.characterShare.setOnLongClickListener { + openLinkInBrowser(link) + true + } + lifecycleScope.launch { + withContext(Dispatchers.IO) { + author.isFav = + Anilist.query.isUserFav(AnilistMutations.FavType.STAFF, author.id) + } + withContext(Dispatchers.Main) { + binding.characterFav.setImageResource( + if (author.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24 + ) + } + } + binding.characterFav.setOnClickListener { + scope.launch { + lifecycleScope.launch { + if (Anilist.mutation.toggleFav(AnilistMutations.FavType.CHARACTER, author.id)) { + author.isFav = !author.isFav + binding.characterFav.setImageResource( + if (author.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24 + ) + } else { + snackString("Failed to toggle favorite") + } + } + } + } model.getAuthor().observe(this) { if (it != null) { author = it loaded = true - binding.studioProgressBar.visibility = View.GONE - binding.studioRecycler.visibility = View.VISIBLE - if (author!!.yearMedia.isNullOrEmpty()) { - binding.studioRecycler.visibility = View.GONE + binding.characterProgress.visibility = View.GONE + binding.characterRecyclerView.visibility = View.VISIBLE + if (author.yearMedia.isNullOrEmpty()) { + binding.characterRecyclerView.visibility = View.GONE } val titlePosition = arrayListOf() val concatAdapter = ConcatAdapter() - val map = author!!.yearMedia ?: return@observe + val map = author.yearMedia ?: return@observe val keys = map.keys.toTypedArray() var pos = 0 @@ -80,6 +152,10 @@ class AuthorActivity : AppCompatActivity() { } } } + val desc = createDesc(author) + val markWon = Markwon.builder(this).usePlugin(SoftBreakAddsNewLinePlugin.create()) + .usePlugin(SpoilerPlugin()).build() + markWon.setMarkdown(binding.authorCharacterDesc, desc) for (i in keys.indices) { val medias = map[keys[i]]!! val empty = if (medias.size >= 4) medias.size % 4 else 4 - medias.size @@ -90,18 +166,18 @@ class AuthorActivity : AppCompatActivity() { concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true)) concatAdapter.addAdapter(EmptyAdapter(empty)) } - binding.studioRecycler.adapter = concatAdapter - binding.studioRecycler.layoutManager = gridLayoutManager + binding.characterRecyclerView.adapter = concatAdapter + binding.characterRecyclerView.layoutManager = gridLayoutManager - binding.charactersRecycler.visibility = View.VISIBLE - binding.charactersText.visibility = View.VISIBLE - binding.charactersRecycler.adapter = - CharacterAdapter(author!!.character ?: arrayListOf()) - binding.charactersRecycler.layoutManager = + binding.authorCharactersRecycler.visibility = View.VISIBLE + binding.AuthorCharactersText.visibility = View.VISIBLE + binding.authorCharactersRecycler.adapter = + CharacterAdapter(author.character ?: arrayListOf()) + binding.authorCharactersRecycler.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) - if (author!!.character.isNullOrEmpty()) { - binding.charactersRecycler.visibility = View.GONE - binding.charactersText.visibility = View.GONE + if (author.character.isNullOrEmpty()) { + binding.authorCharactersRecycler.visibility = View.GONE + binding.AuthorCharactersText.visibility = View.GONE } } } @@ -109,14 +185,28 @@ class AuthorActivity : AppCompatActivity() { live.observe(this) { if (it) { scope.launch { - if (author != null) - withContext(Dispatchers.IO) { model.loadAuthor(author!!) } + withContext(Dispatchers.IO) { model.loadAuthor(author) } live.postValue(false) } } } } + private fun createDesc(author: Author): String { + val age = if (author.age != null) "${getString(R.string.age)} ${author.age}" else "" + val yearsActive = + if (author.yearsActive != null) "${getString(R.string.years_active)} ${author.yearsActive}" else "" + val dob = + if (author.dateOfBirth != null) "${getString(R.string.birthday)} ${author.dateOfBirth}" else "" + val homeTown = + if (author.homeTown != null) "${getString(R.string.hometown)} ${author.homeTown}" else "" + val dod = + if (author.dateOfDeath != null) "${getString(R.string.date_of_death)} ${author.dateOfDeath}" else "" + + return "$age $yearsActive $dob $homeTown $dod" + } + + override fun onDestroy() { if (Refresh.activity.containsKey(this.hashCode())) { Refresh.activity.remove(this.hashCode()) @@ -125,7 +215,31 @@ class AuthorActivity : AppCompatActivity() { } override fun onResume() { - binding.studioProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE + binding.characterProgress.visibility = if (!loaded) View.VISIBLE else View.GONE super.onResume() } + + override fun onOffsetChanged(appBar: AppBarLayout, i: Int) { + if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange + val percentage = abs(i) * 100 / mMaxScrollSize + val cap = clamp((percent - percentage) / percent.toFloat(), 0f, 1f) + + binding.characterCover.scaleX = 1f * cap + binding.characterCover.scaleY = 1f * cap + binding.characterCover.cardElevation = 32f * cap + + binding.characterCover.visibility = + if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE + val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode) + if (percentage >= percent && !isCollapsed) { + isCollapsed = true + if (immersiveMode) this.window.statusBarColor = + ContextCompat.getColor(this, R.color.nav_bg) + } + if (percentage <= percent && isCollapsed) { + isCollapsed = false + if (immersiveMode) this.window.statusBarColor = + ContextCompat.getColor(this, R.color.transparent) + } + } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt b/app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt index 7b0953fc..d37093ca 100644 --- a/app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt @@ -15,7 +15,7 @@ import ani.dantotsu.setAnimation import java.io.Serializable class AuthorAdapter( - private val authorList: ArrayList, + private val authorList: MutableList, ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder { val binding = @@ -26,7 +26,7 @@ class AuthorAdapter( override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) { val binding = holder.binding setAnimation(binding.root.context, holder.binding.root) - val author = authorList[position] + val author = authorList.getOrNull(position) ?: return binding.itemCompactRelation.text = author.role binding.itemCompactImage.loadImage(author.image) binding.itemCompactTitle.text = author.name diff --git a/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt b/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt index 2186791d..c3de3df3 100644 --- a/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt @@ -16,7 +16,7 @@ import ani.dantotsu.setAnimation import java.io.Serializable class CharacterAdapter( - private val characterList: ArrayList + private val characterList: MutableList ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder { val binding = @@ -27,9 +27,8 @@ class CharacterAdapter( override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) { val binding = holder.binding setAnimation(binding.root.context, holder.binding.root) - val character = characterList[position] - val whitespace = "${character.role} " - character.voiceActor + val character = characterList.getOrNull(position) ?: return + val whitespace = "${if (character.role.lowercase() == "null") "" else character.role} " binding.itemCompactRelation.text = whitespace binding.itemCompactImage.loadImage(character.image) binding.itemCompactTitle.text = character.name diff --git a/app/src/main/java/ani/dantotsu/media/CharacterDetailsActivity.kt b/app/src/main/java/ani/dantotsu/media/CharacterDetailsActivity.kt index 323b689e..ad90ee6b 100644 --- a/app/src/main/java/ani/dantotsu/media/CharacterDetailsActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/CharacterDetailsActivity.kt @@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.math.MathUtils.clamp import androidx.core.view.isGone +import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.lifecycle.MutableLiveData @@ -45,6 +46,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang private lateinit var character: Character private var loaded = false + private var isCollapsed = false + private val percent = 30 + private var mMaxScrollSize = 0 + private var screenWidth: Float = 0f + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -71,6 +77,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang binding.characterClose.setOnClickListener { onBackPressedDispatcher.onBackPressed() } + + binding.authorCharactersRecycler.isVisible = false + binding.AuthorCharactersText.isVisible = false + binding.authorCharacterDesc.isVisible = false + character = intent.getSerialized("character") ?: return binding.characterTitle.text = character.name banner.loadImage(character.banner) @@ -158,11 +169,6 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang super.onResume() } - private var isCollapsed = false - private val percent = 30 - private var mMaxScrollSize = 0 - private var screenWidth: Float = 0f - override fun onOffsetChanged(appBar: AppBarLayout, i: Int) { if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange val percentage = abs(i) * 100 / mMaxScrollSize diff --git a/app/src/main/java/ani/dantotsu/media/HeaderInterface.kt b/app/src/main/java/ani/dantotsu/media/HeaderInterface.kt new file mode 100644 index 00000000..46937ef0 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/media/HeaderInterface.kt @@ -0,0 +1,77 @@ +package ani.dantotsu.media + +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.AlphaAnimation +import android.view.animation.Animation +import androidx.recyclerview.widget.RecyclerView +import ani.dantotsu.databinding.ItemSearchHeaderBinding + +abstract class HeaderInterface: RecyclerView.Adapter() { + private val itemViewType = 6969 + var search: Runnable? = null + var requestFocus: Runnable? = null + protected var textWatcher: TextWatcher? = null + protected lateinit var searchHistoryAdapter: SearchHistoryAdapter + protected lateinit var binding: ItemSearchHeaderBinding + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder { + val binding = + ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return SearchHeaderViewHolder(binding) + } + + fun setHistoryVisibility(visible: Boolean) { + if (visible) { + binding.searchResultLayout.startAnimation(fadeOutAnimation()) + binding.searchHistoryList.startAnimation(fadeInAnimation()) + binding.searchResultLayout.visibility = View.GONE + binding.searchHistoryList.visibility = View.VISIBLE + binding.searchByImage.visibility = View.VISIBLE + } else { + if (binding.searchResultLayout.visibility != View.VISIBLE) { + binding.searchResultLayout.startAnimation(fadeInAnimation()) + binding.searchHistoryList.startAnimation(fadeOutAnimation()) + } + + binding.searchResultLayout.visibility = View.VISIBLE + binding.clearHistory.visibility = View.GONE + binding.searchHistoryList.visibility = View.GONE + binding.searchByImage.visibility = View.GONE + } + } + + private fun fadeInAnimation(): Animation { + return AlphaAnimation(0f, 1f).apply { + duration = 150 + } + } + + protected fun fadeOutAnimation(): Animation { + return AlphaAnimation(1f, 0f).apply { + duration = 150 + } + } + + protected fun updateClearHistoryVisibility() { + binding.clearHistory.visibility = + if (searchHistoryAdapter.itemCount > 0) View.VISIBLE else View.GONE + } + + fun addHistory() { + if (::searchHistoryAdapter.isInitialized && binding.searchBarText.text.toString() + .isNotBlank() + ) searchHistoryAdapter.add(binding.searchBarText.text.toString()) + } + + inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) : + RecyclerView.ViewHolder(binding.root) + + override fun getItemCount(): Int = 1 + + override fun getItemViewType(position: Int): Int { + return itemViewType + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/SearchActivity.kt b/app/src/main/java/ani/dantotsu/media/SearchActivity.kt index 358c9700..b4e16391 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchActivity.kt @@ -15,10 +15,16 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.AnilistSearch -import ani.dantotsu.connections.anilist.SearchResults +import ani.dantotsu.connections.anilist.AnilistSearch.SearchType +import ani.dantotsu.connections.anilist.AniMangaSearchResults +import ani.dantotsu.connections.anilist.CharacterSearchResults +import ani.dantotsu.connections.anilist.StaffSearchResults +import ani.dantotsu.connections.anilist.StudioSearchResults +import ani.dantotsu.connections.anilist.UserSearchResults import ani.dantotsu.databinding.ActivitySearchBinding import ani.dantotsu.initActivity import ani.dantotsu.navBarHeight +import ani.dantotsu.profile.UsersAdapter import ani.dantotsu.px import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName @@ -35,14 +41,25 @@ class SearchActivity : AppCompatActivity() { val model: AnilistSearch by viewModels() var style: Int = 0 + lateinit var searchType: SearchType private var screenWidth: Float = 0f private lateinit var mediaAdaptor: MediaAdaptor + private lateinit var characterAdaptor: CharacterAdapter + private lateinit var studioAdaptor: StudioAdapter + private lateinit var staffAdaptor: AuthorAdapter + private lateinit var usersAdapter: UsersAdapter + private lateinit var progressAdapter: ProgressAdapter private lateinit var concatAdapter: ConcatAdapter - private lateinit var headerAdaptor: SearchAdapter + private lateinit var headerAdaptor: HeaderInterface + + lateinit var aniMangaResult: AniMangaSearchResults + lateinit var characterResult: CharacterSearchResults + lateinit var studioResult: StudioSearchResults + lateinit var staffResult: StaffSearchResults + lateinit var userResult: UserSearchResults - lateinit var result: SearchResults lateinit var updateChips: (() -> Unit) override fun onCreate(savedInstanceState: Bundle?) { @@ -59,39 +76,117 @@ class SearchActivity : AppCompatActivity() { bottom = navBarHeight + 80f.px ) - style = PrefManager.getVal(PrefName.SearchStyle) - var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false) - if (!listOnly!!) listOnly = null - val notSet = model.notSet - if (model.notSet) { - model.notSet = false - model.searchResults = SearchResults( - intent.getStringExtra("type") ?: "ANIME", - isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false, - onList = listOnly, - search = intent.getStringExtra("query"), - genres = intent.getStringExtra("genre")?.let { mutableListOf(it) }, - tags = intent.getStringExtra("tag")?.let { mutableListOf(it) }, - sort = intent.getStringExtra("sortBy"), - status = intent.getStringExtra("status"), - source = intent.getStringExtra("source"), - countryOfOrigin = intent.getStringExtra("country"), - season = intent.getStringExtra("season"), - seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear") - ?.toIntOrNull() else null, - startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear") - ?.toIntOrNull() else null, - results = mutableListOf(), - hasNextPage = false - ) + searchType = SearchType.fromString(intent.getStringExtra("type") ?: "ANIME") + when (searchType) { + SearchType.ANIME, SearchType.MANGA -> { + style = PrefManager.getVal(PrefName.SearchStyle) + var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false) + if (!listOnly!!) listOnly = null + + if (model.notSet) { + model.notSet = false + model.aniMangaSearchResults = AniMangaSearchResults( + intent.getStringExtra("type") ?: "ANIME", + isAdult = if (Anilist.adult) intent.getBooleanExtra( + "hentai", + false + ) else false, + onList = listOnly, + search = intent.getStringExtra("query"), + genres = intent.getStringExtra("genre")?.let { mutableListOf(it) }, + tags = intent.getStringExtra("tag")?.let { mutableListOf(it) }, + sort = intent.getStringExtra("sortBy"), + status = intent.getStringExtra("status"), + source = intent.getStringExtra("source"), + countryOfOrigin = intent.getStringExtra("country"), + season = intent.getStringExtra("season"), + seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra( + "seasonYear" + ) + ?.toIntOrNull() else null, + startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra( + "seasonYear" + ) + ?.toIntOrNull() else null, + results = mutableListOf(), + hasNextPage = false + ) + } + + aniMangaResult = model.aniMangaSearchResults + mediaAdaptor = + MediaAdaptor( + style, + model.aniMangaSearchResults.results, + this, + matchParent = true + ) + } + + SearchType.CHARACTER -> { + if (model.notSet) { + model.notSet = false + model.characterSearchResults = CharacterSearchResults( + search = intent.getStringExtra("query"), + results = mutableListOf(), + hasNextPage = false + ) + + characterResult = model.characterSearchResults + characterAdaptor = CharacterAdapter(model.characterSearchResults.results) + } + } + + SearchType.STUDIO -> { + if (model.notSet) { + model.notSet = false + model.studioSearchResults = StudioSearchResults( + search = intent.getStringExtra("query"), + results = mutableListOf(), + hasNextPage = false + ) + + studioResult = model.studioSearchResults + studioAdaptor = StudioAdapter(model.studioSearchResults.results) + } + } + + SearchType.STAFF -> { + if (model.notSet) { + model.notSet = false + model.staffSearchResults = StaffSearchResults( + search = intent.getStringExtra("query"), + results = mutableListOf(), + hasNextPage = false + ) + + staffResult = model.staffSearchResults + staffAdaptor = AuthorAdapter(model.staffSearchResults.results) + } + } + + SearchType.USER -> { + if (model.notSet) { + model.notSet = false + model.userSearchResults = UserSearchResults( + search = intent.getStringExtra("query"), + results = mutableListOf(), + hasNextPage = false + ) + + userResult = model.userSearchResults + usersAdapter = UsersAdapter(model.userSearchResults.results, grid = true) + } + } } - result = model.searchResults - progressAdapter = ProgressAdapter(searched = model.searched) - mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true) - headerAdaptor = SearchAdapter(this, model.searchResults.type) + headerAdaptor = if (searchType == SearchType.ANIME || searchType == SearchType.MANGA) { + SearchAdapter(this, searchType) + } else { + SupportingSearchAdapter(this, searchType) + } val gridSize = (screenWidth / 120f).toInt() val gridLayoutManager = GridLayoutManager(this, gridSize) @@ -108,7 +203,27 @@ class SearchActivity : AppCompatActivity() { } } - concatAdapter = ConcatAdapter(headerAdaptor, mediaAdaptor, progressAdapter) + concatAdapter = when (searchType) { + SearchType.ANIME, SearchType.MANGA -> { + ConcatAdapter(headerAdaptor, mediaAdaptor, progressAdapter) + } + + SearchType.CHARACTER -> { + ConcatAdapter(headerAdaptor, characterAdaptor, progressAdapter) + } + + SearchType.STUDIO -> { + ConcatAdapter(headerAdaptor, studioAdaptor, progressAdapter) + } + + SearchType.STAFF -> { + ConcatAdapter(headerAdaptor, staffAdaptor, progressAdapter) + } + + SearchType.USER -> { + ConcatAdapter(headerAdaptor, usersAdapter, progressAdapter) + } + } binding.searchRecyclerView.layoutManager = gridLayoutManager binding.searchRecyclerView.adapter = concatAdapter @@ -117,9 +232,9 @@ class SearchActivity : AppCompatActivity() { RecyclerView.OnScrollListener() { override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) { if (!v.canScrollVertically(1)) { - if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) { + if (model.hasNextPage(searchType) && model.resultsIsNotEmpty(searchType) && !loading) { scope.launch(Dispatchers.IO) { - model.loadNextPage(model.searchResults) + model.loadNextPage(searchType) } } } @@ -127,34 +242,110 @@ class SearchActivity : AppCompatActivity() { } }) - model.getSearch().observe(this) { - if (it != null) { - model.searchResults.apply { - onList = it.onList - isAdult = it.isAdult - perPage = it.perPage - search = it.search - sort = it.sort - genres = it.genres - excludedGenres = it.excludedGenres - excludedTags = it.excludedTags - tags = it.tags - season = it.season - startYear = it.startYear - seasonYear = it.seasonYear - status = it.status - source = it.source - format = it.format - countryOfOrigin = it.countryOfOrigin - page = it.page - hasNextPage = it.hasNextPage + when (searchType) { + SearchType.ANIME, SearchType.MANGA -> { + model.getSearch(searchType).observe(this) { + if (it != null) { + model.aniMangaSearchResults.apply { + onList = it.onList + isAdult = it.isAdult + perPage = it.perPage + search = it.search + sort = it.sort + genres = it.genres + excludedGenres = it.excludedGenres + excludedTags = it.excludedTags + tags = it.tags + season = it.season + startYear = it.startYear + seasonYear = it.seasonYear + status = it.status + source = it.source + format = it.format + countryOfOrigin = it.countryOfOrigin + page = it.page + hasNextPage = it.hasNextPage + } + + val prev = model.aniMangaSearchResults.results.size + model.aniMangaSearchResults.results.addAll(it.results) + mediaAdaptor.notifyItemRangeInserted(prev, it.results.size) + + progressAdapter.bar?.isVisible = it.hasNextPage + } } + } - val prev = model.searchResults.results.size - model.searchResults.results.addAll(it.results) - mediaAdaptor.notifyItemRangeInserted(prev, it.results.size) + SearchType.CHARACTER -> { + model.getSearch(searchType).observe(this) { + if (it != null) { + model.characterSearchResults.apply { + search = it.search + page = it.page + hasNextPage = it.hasNextPage + } - progressAdapter.bar?.isVisible = it.hasNextPage + val prev = model.characterSearchResults.results.size + model.characterSearchResults.results.addAll(it.results) + characterAdaptor.notifyItemRangeInserted(prev, it.results.size) + + progressAdapter.bar?.isVisible = it.hasNextPage + } + } + } + + SearchType.STUDIO -> { + model.getSearch(searchType).observe(this) { + if (it != null) { + model.studioSearchResults.apply { + search = it.search + page = it.page + hasNextPage = it.hasNextPage + } + + val prev = model.studioSearchResults.results.size + model.studioSearchResults.results.addAll(it.results) + studioAdaptor.notifyItemRangeInserted(prev, it.results.size) + + progressAdapter.bar?.isVisible = it.hasNextPage + } + } + } + + SearchType.STAFF -> { + model.getSearch(searchType).observe(this) { + if (it != null) { + model.staffSearchResults.apply { + search = it.search + page = it.page + hasNextPage = it.hasNextPage + } + + val prev = model.staffSearchResults.results.size + model.staffSearchResults.results.addAll(it.results) + staffAdaptor.notifyItemRangeInserted(prev, it.results.size) + + progressAdapter.bar?.isVisible = it.hasNextPage + } + } + } + + SearchType.USER -> { + model.getSearch(searchType).observe(this) { + if (it != null) { + model.userSearchResults.apply { + search = it.search + page = it.page + hasNextPage = it.hasNextPage + } + + val prev = model.userSearchResults.results.size + model.userSearchResults.results.addAll(it.results) + usersAdapter.notifyItemRangeInserted(prev, it.results.size) + + progressAdapter.bar?.isVisible = it.hasNextPage + } + } } } @@ -179,8 +370,32 @@ class SearchActivity : AppCompatActivity() { fun emptyMediaAdapter() { searchTimer.cancel() searchTimer.purge() - mediaAdaptor.notifyItemRangeRemoved(0, model.searchResults.results.size) - model.searchResults.results.clear() + when (searchType) { + SearchType.ANIME, SearchType.MANGA -> { + mediaAdaptor.notifyItemRangeRemoved(0, model.aniMangaSearchResults.results.size) + model.aniMangaSearchResults.results.clear() + } + + SearchType.CHARACTER -> { + characterAdaptor.notifyItemRangeRemoved(0, model.characterSearchResults.results.size) + model.characterSearchResults.results.clear() + } + + SearchType.STUDIO -> { + studioAdaptor.notifyItemRangeRemoved(0, model.studioSearchResults.results.size) + model.studioSearchResults.results.clear() + } + + SearchType.STAFF -> { + staffAdaptor.notifyItemRangeRemoved(0, model.staffSearchResults.results.size) + model.staffSearchResults.results.clear() + } + + SearchType.USER -> { + usersAdapter.notifyItemRangeRemoved(0, model.userSearchResults.results.size) + model.userSearchResults.results.clear() + } + } progressAdapter.bar?.visibility = View.GONE } @@ -188,10 +403,30 @@ class SearchActivity : AppCompatActivity() { private var loading = false fun search() { headerAdaptor.setHistoryVisibility(false) - val size = model.searchResults.results.size - model.searchResults.results.clear() + val size = model.size(searchType) + model.clearResults(searchType) binding.searchRecyclerView.post { - mediaAdaptor.notifyItemRangeRemoved(0, size) + when (searchType) { + SearchType.ANIME, SearchType.MANGA -> { + mediaAdaptor.notifyItemRangeRemoved(0, size) + } + + SearchType.CHARACTER -> { + characterAdaptor.notifyItemRangeRemoved(0, size) + } + + SearchType.STUDIO -> { + studioAdaptor.notifyItemRangeRemoved(0, size) + } + + SearchType.STAFF -> { + staffAdaptor.notifyItemRangeRemoved(0, size) + } + + SearchType.USER -> { + usersAdapter.notifyItemRangeRemoved(0, size) + } + } } progressAdapter.bar?.visibility = View.VISIBLE @@ -202,7 +437,7 @@ class SearchActivity : AppCompatActivity() { override fun run() { scope.launch(Dispatchers.IO) { loading = true - model.loadSearch(result) + model.loadSearch(searchType) loading = false } } @@ -213,8 +448,10 @@ class SearchActivity : AppCompatActivity() { @SuppressLint("NotifyDataSetChanged") fun recycler() { - mediaAdaptor.type = style - mediaAdaptor.notifyDataSetChanged() + if (searchType == SearchType.ANIME || searchType == SearchType.MANGA) { + mediaAdaptor.type = style + mediaAdaptor.notifyDataSetChanged() + } } var state: Parcelable? = null diff --git a/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt b/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt index 21f0b683..15081d35 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt @@ -9,8 +9,6 @@ import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup -import android.view.animation.AlphaAnimation -import android.view.animation.Animation import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.PopupMenu @@ -22,8 +20,8 @@ import androidx.recyclerview.widget.RecyclerView.HORIZONTAL import ani.dantotsu.App.Companion.context import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist +import ani.dantotsu.connections.anilist.AnilistSearch.SearchType import ani.dantotsu.databinding.ItemChipBinding -import ani.dantotsu.databinding.ItemSearchHeaderBinding import ani.dantotsu.openLinkInBrowser import ani.dantotsu.others.imagesearch.ImageSearchActivity import ani.dantotsu.settings.saving.PrefManager @@ -36,18 +34,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch - -class SearchAdapter(private val activity: SearchActivity, private val type: String) : - RecyclerView.Adapter() { - private val itemViewType = 6969 - var search: Runnable? = null - var requestFocus: Runnable? = null - private var textWatcher: TextWatcher? = null - private lateinit var searchHistoryAdapter: SearchHistoryAdapter - private lateinit var binding: ItemSearchHeaderBinding +class SearchAdapter(private val activity: SearchActivity, private val type: SearchType) : + HeaderInterface() { private fun updateFilterTextViewDrawable() { - val filterDrawable = when (activity.result.sort) { + val filterDrawable = when (activity.aniMangaResult.sort) { Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24 Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24 Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24 @@ -60,12 +51,6 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri binding.filterTextView.setCompoundDrawablesWithIntrinsicBounds(filterDrawable, 0, 0, 0) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder { - val binding = - ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return SearchHeaderViewHolder(binding) - } - @SuppressLint("ClickableViewAccessibility") override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) { binding = holder.binding @@ -79,6 +64,10 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri val imm: InputMethodManager = activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager + if (activity.searchType != SearchType.MANGA && activity.searchType != SearchType.ANIME) { + throw IllegalArgumentException("Invalid search type (wrong adapter)") + } + when (activity.style) { 0 -> { binding.searchResultGrid.alpha = 1f @@ -91,7 +80,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri } } - binding.searchBar.hint = activity.result.type + binding.searchBar.hint = activity.aniMangaResult.type if (PrefManager.getVal(PrefName.Incognito)) { val startIconDrawableRes = R.drawable.ic_incognito_24 val startIconDrawable: Drawable? = @@ -99,11 +88,11 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri binding.searchBar.startIconDrawable = startIconDrawable } - var adult = activity.result.isAdult - var listOnly = activity.result.onList + var adult = activity.aniMangaResult.isAdult + var listOnly = activity.aniMangaResult.onList binding.searchBarText.removeTextChangedListener(textWatcher) - binding.searchBarText.setText(activity.result.search) + binding.searchBarText.setText(activity.aniMangaResult.search) binding.searchAdultCheck.isChecked = adult binding.searchList.isChecked = listOnly == true @@ -124,49 +113,49 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri popupMenu.setOnMenuItemClickListener { item -> when (item.itemId) { R.id.sort_by_score -> { - activity.result.sort = Anilist.sortBy[0] + activity.aniMangaResult.sort = Anilist.sortBy[0] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } R.id.sort_by_popular -> { - activity.result.sort = Anilist.sortBy[1] + activity.aniMangaResult.sort = Anilist.sortBy[1] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } R.id.sort_by_trending -> { - activity.result.sort = Anilist.sortBy[2] + activity.aniMangaResult.sort = Anilist.sortBy[2] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } R.id.sort_by_recent -> { - activity.result.sort = Anilist.sortBy[3] + activity.aniMangaResult.sort = Anilist.sortBy[3] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } R.id.sort_by_a_z -> { - activity.result.sort = Anilist.sortBy[4] + activity.aniMangaResult.sort = Anilist.sortBy[4] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } R.id.sort_by_z_a -> { - activity.result.sort = Anilist.sortBy[5] + activity.aniMangaResult.sort = Anilist.sortBy[5] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } R.id.sort_by_pure_pain -> { - activity.result.sort = Anilist.sortBy[6] + activity.aniMangaResult.sort = Anilist.sortBy[6] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() @@ -177,7 +166,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri popupMenu.show() true } - if (activity.result.type != "ANIME") { + if (activity.aniMangaResult.type != "ANIME") { binding.searchByImage.visibility = View.GONE } binding.searchByImage.setOnClickListener { @@ -190,7 +179,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri } updateClearHistoryVisibility() fun searchTitle() { - activity.result.apply { + activity.aniMangaResult.apply { search = if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null onList = listOnly @@ -292,67 +281,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri requestFocus = Runnable { binding.searchBarText.requestFocus() } } - fun setHistoryVisibility(visible: Boolean) { - if (visible) { - binding.searchResultLayout.startAnimation(fadeOutAnimation()) - binding.searchHistoryList.startAnimation(fadeInAnimation()) - binding.searchResultLayout.visibility = View.GONE - binding.searchHistoryList.visibility = View.VISIBLE - binding.searchByImage.visibility = View.VISIBLE - } else { - if (binding.searchResultLayout.visibility != View.VISIBLE) { - binding.searchResultLayout.startAnimation(fadeInAnimation()) - binding.searchHistoryList.startAnimation(fadeOutAnimation()) - } - - binding.searchResultLayout.visibility = View.VISIBLE - binding.clearHistory.visibility = View.GONE - binding.searchHistoryList.visibility = View.GONE - binding.searchByImage.visibility = View.GONE - } - } - - private fun updateClearHistoryVisibility() { - binding.clearHistory.visibility = - if (searchHistoryAdapter.itemCount > 0) View.VISIBLE else View.GONE - } - - private fun fadeInAnimation(): Animation { - return AlphaAnimation(0f, 1f).apply { - duration = 150 - } - } - - private fun fadeOutAnimation(): Animation { - return AlphaAnimation(1f, 0f).apply { - duration = 150 - } - } - - - fun addHistory() { - if (::searchHistoryAdapter.isInitialized && - binding.searchBarText.text.toString().isNotBlank() - ) - searchHistoryAdapter.add(binding.searchBarText.text.toString()) - } - - override fun getItemCount(): Int = 1 - - inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) : - RecyclerView.ViewHolder(binding.root) - - override fun getItemViewType(position: Int): Int { - return itemViewType - } - - class SearchChipAdapter( val activity: SearchActivity, private val searchAdapter: SearchAdapter ) : RecyclerView.Adapter() { - private var chips = activity.result.toChipList() + private var chips = activity.aniMangaResult.toChipList() inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root) @@ -369,7 +303,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri holder.binding.root.apply { text = chip.text.replace("_", " ") setOnClickListener { - activity.result.removeChip(chip) + activity.aniMangaResult.removeChip(chip) update() activity.search() searchAdapter.updateFilterTextViewDrawable() @@ -379,7 +313,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri @SuppressLint("NotifyDataSetChanged") fun update() { - chips = activity.result.toChipList() + chips = activity.aniMangaResult.toChipList() notifyDataSetChanged() searchAdapter.updateFilterTextViewDrawable() } diff --git a/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt b/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt index 6658532c..f9c08d5e 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt @@ -57,7 +57,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { } private fun setSortByFilterImage() { - val filterDrawable = when (activity.result.sort) { + val filterDrawable = when (activity.aniMangaResult.sort) { Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24 Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24 Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24 @@ -71,10 +71,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { } private fun resetSearchFilter() { - activity.result.sort = null + activity.aniMangaResult.sort = null binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24) startBounceZoomAnimation(binding.sortByFilter) - activity.result.countryOfOrigin = null + activity.aniMangaResult.countryOfOrigin = null binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts) startBounceZoomAnimation(binding.countryFilter) @@ -98,10 +98,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { activity = requireActivity() as SearchActivity - selectedGenres = activity.result.genres ?: mutableListOf() - exGenres = activity.result.excludedGenres ?: mutableListOf() - selectedTags = activity.result.tags ?: mutableListOf() - exTags = activity.result.excludedTags ?: mutableListOf() + selectedGenres = activity.aniMangaResult.genres ?: mutableListOf() + exGenres = activity.aniMangaResult.excludedGenres ?: mutableListOf() + selectedTags = activity.aniMangaResult.tags ?: mutableListOf() + exTags = activity.aniMangaResult.excludedTags ?: mutableListOf() setSortByFilterImage() binding.resetSearchFilter.setOnClickListener { @@ -126,7 +126,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { resetSearchFilter() CoroutineScope(Dispatchers.Main).launch { - activity.result.apply { + activity.aniMangaResult.apply { status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null } source = @@ -135,7 +135,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { season = binding.searchSeason.text.toString().ifBlank { null } startYear = binding.searchYear.text.toString().toIntOrNull() seasonYear = binding.searchYear.text.toString().toIntOrNull() - sort = activity.result.sort + sort = activity.aniMangaResult.sort genres = selectedGenres tags = selectedTags excludedGenres = exGenres @@ -155,43 +155,43 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { popupMenu.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.sort_by_score -> { - activity.result.sort = Anilist.sortBy[0] + activity.aniMangaResult.sort = Anilist.sortBy[0] binding.sortByFilter.setImageResource(R.drawable.ic_round_area_chart_24) startBounceZoomAnimation() } R.id.sort_by_popular -> { - activity.result.sort = Anilist.sortBy[1] + activity.aniMangaResult.sort = Anilist.sortBy[1] binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_peak_24) startBounceZoomAnimation() } R.id.sort_by_trending -> { - activity.result.sort = Anilist.sortBy[2] + activity.aniMangaResult.sort = Anilist.sortBy[2] binding.sortByFilter.setImageResource(R.drawable.ic_round_star_graph_24) startBounceZoomAnimation() } R.id.sort_by_recent -> { - activity.result.sort = Anilist.sortBy[3] + activity.aniMangaResult.sort = Anilist.sortBy[3] binding.sortByFilter.setImageResource(R.drawable.ic_round_new_releases_24) startBounceZoomAnimation() } R.id.sort_by_a_z -> { - activity.result.sort = Anilist.sortBy[4] + activity.aniMangaResult.sort = Anilist.sortBy[4] binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24) startBounceZoomAnimation() } R.id.sort_by_z_a -> { - activity.result.sort = Anilist.sortBy[5] + activity.aniMangaResult.sort = Anilist.sortBy[5] binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24_reverse) startBounceZoomAnimation() } R.id.sort_by_pure_pain -> { - activity.result.sort = Anilist.sortBy[6] + activity.aniMangaResult.sort = Anilist.sortBy[6] binding.sortByFilter.setImageResource(R.drawable.ic_round_assist_walker_24) startBounceZoomAnimation() } @@ -212,25 +212,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { } R.id.country_china -> { - activity.result.countryOfOrigin = "CN" + activity.aniMangaResult.countryOfOrigin = "CN" binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts) startBounceZoomAnimation(binding.countryFilter) } R.id.country_south_korea -> { - activity.result.countryOfOrigin = "KR" + activity.aniMangaResult.countryOfOrigin = "KR" binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts) startBounceZoomAnimation(binding.countryFilter) } R.id.country_japan -> { - activity.result.countryOfOrigin = "JP" + activity.aniMangaResult.countryOfOrigin = "JP" binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts) startBounceZoomAnimation(binding.countryFilter) } R.id.country_taiwan -> { - activity.result.countryOfOrigin = "TW" + activity.aniMangaResult.countryOfOrigin = "TW" binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts) startBounceZoomAnimation(binding.countryFilter) } @@ -241,18 +241,18 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { } binding.searchFilterApply.setOnClickListener { - activity.result.apply { + activity.aniMangaResult.apply { status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null } source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null } format = binding.searchFormat.text.toString().ifBlank { null } season = binding.searchSeason.text.toString().ifBlank { null } - if (activity.result.type == "ANIME") { + if (activity.aniMangaResult.type == "ANIME") { seasonYear = binding.searchYear.text.toString().toIntOrNull() } else { startYear = binding.searchYear.text.toString().toIntOrNull() } - sort = activity.result.sort - countryOfOrigin = activity.result.countryOfOrigin + sort = activity.aniMangaResult.sort + countryOfOrigin = activity.aniMangaResult.countryOfOrigin genres = selectedGenres tags = selectedTags excludedGenres = exGenres @@ -266,8 +266,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { dismiss() } val format = - if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus - binding.searchStatus.setText(activity.result.status?.replace("_", " ")) + if (activity.aniMangaResult.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus + binding.searchStatus.setText(activity.aniMangaResult.status?.replace("_", " ")) binding.searchStatus.setAdapter( ArrayAdapter( binding.root.context, @@ -276,7 +276,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { ) ) - binding.searchSource.setText(activity.result.source?.replace("_", " ")) + binding.searchSource.setText(activity.aniMangaResult.source?.replace("_", " ")) binding.searchSource.setAdapter( ArrayAdapter( binding.root.context, @@ -285,19 +285,19 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { ) ) - binding.searchFormat.setText(activity.result.format) + binding.searchFormat.setText(activity.aniMangaResult.format) binding.searchFormat.setAdapter( ArrayAdapter( binding.root.context, R.layout.item_dropdown, - (if (activity.result.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray() + (if (activity.aniMangaResult.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray() ) ) - if (activity.result.type == "ANIME") { - binding.searchYear.setText(activity.result.seasonYear?.toString()) + if (activity.aniMangaResult.type == "ANIME") { + binding.searchYear.setText(activity.aniMangaResult.seasonYear?.toString()) } else { - binding.searchYear.setText(activity.result.startYear?.toString()) + binding.searchYear.setText(activity.aniMangaResult.startYear?.toString()) } binding.searchYear.setAdapter( ArrayAdapter( @@ -308,9 +308,9 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { ) ) - if (activity.result.type == "MANGA") binding.searchSeasonCont.visibility = GONE + if (activity.aniMangaResult.type == "MANGA") binding.searchSeasonCont.visibility = GONE else { - binding.searchSeason.setText(activity.result.season) + binding.searchSeason.setText(activity.aniMangaResult.season) binding.searchSeason.setAdapter( ArrayAdapter( binding.root.context, @@ -346,7 +346,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { binding.searchGenresGrid.isChecked = false binding.searchFilterTags.adapter = - FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip -> + FilterChipAdapter(Anilist.tags?.get(activity.aniMangaResult.isAdult) ?: listOf()) { chip -> val tag = chip.text.toString() chip.isChecked = selectedTags.contains(tag) chip.isCloseIconVisible = exTags.contains(tag) diff --git a/app/src/main/java/ani/dantotsu/media/SearchHistoryAdapter.kt b/app/src/main/java/ani/dantotsu/media/SearchHistoryAdapter.kt index 4e2988e3..f6d46583 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchHistoryAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchHistoryAdapter.kt @@ -7,52 +7,73 @@ 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.PrefManager.asLiveClass import ani.dantotsu.settings.saving.PrefName -import ani.dantotsu.settings.saving.SharedPreferenceStringSetLiveData -import java.util.Locale +import ani.dantotsu.settings.saving.SharedPreferenceClassLiveData +import java.io.Serializable -class SearchHistoryAdapter(private val type: String, private val searchClicked: (String) -> Unit) : +data class SearchHistory(val search: String, val time: Long) : Serializable { + companion object { + private const val serialVersionUID = 1L + } +} + +class SearchHistoryAdapter(type: SearchType, private val searchClicked: (String) -> Unit) : ListAdapter( DIFF_CALLBACK_INSTALLED ) { - private var searchHistoryLiveData: SharedPreferenceStringSetLiveData? = null - private var searchHistory: MutableSet? = null - private var historyType: PrefName = when (type.lowercase(Locale.ROOT)) { - "anime" -> PrefName.AnimeSearchHistory - "manga" -> PrefName.MangaSearchHistory - else -> throw IllegalArgumentException("Invalid type") + private var searchHistoryLiveData: SharedPreferenceClassLiveData>? = null + private var searchHistory: MutableList? = null + private var historyType: PrefName = when (type) { + SearchType.ANIME -> PrefName.SortedAnimeSH + SearchType.MANGA -> PrefName.SortedMangaSH + SearchType.CHARACTER -> PrefName.SortedCharacterSH + SearchType.STAFF -> PrefName.SortedStaffSH + SearchType.STUDIO -> PrefName.SortedStudioSH + SearchType.USER -> PrefName.SortedUserSH } + private fun MutableList?.sorted(): List? = + this?.sortedByDescending { it.time }?.map { it.search } + init { searchHistoryLiveData = - PrefManager.getLiveVal(historyType, mutableSetOf()).asLiveStringSet() - searchHistoryLiveData?.observeForever { - searchHistory = it.toMutableSet() - submitList(searchHistory?.toList()) + PrefManager.getLiveVal(historyType, mutableListOf()).asLiveClass() + searchHistoryLiveData?.observeForever { data -> + searchHistory = data.toMutableList() + submitList(searchHistory?.sorted()) } } fun remove(item: String) { - searchHistory?.remove(item) + searchHistory?.let { list -> + list.removeAll { it.search == item } + } PrefManager.setVal(historyType, searchHistory) - submitList(searchHistory?.toList()) + submitList(searchHistory?.sorted()) } fun add(item: String) { - if (searchHistory?.contains(item) == true || item.isBlank()) return + val maxSize = 25 + if (searchHistory?.any { it.search == item } == true || item.isBlank()) return if (PrefManager.getVal(PrefName.Incognito)) return - searchHistory?.add(item) - submitList(searchHistory?.toList()) + searchHistory?.add(SearchHistory(item, System.currentTimeMillis())) + if ((searchHistory?.size ?: 0) > maxSize) { + searchHistory?.removeAt( + searchHistory?.sorted()?.lastIndex ?: 0 + ) + } + submitList(searchHistory?.sorted()) PrefManager.setVal(historyType, searchHistory) } fun clearHistory() { searchHistory?.clear() PrefManager.setVal(historyType, searchHistory) - submitList(searchHistory?.toList()) + submitList(searchHistory?.sorted()) } override fun onCreateViewHolder( diff --git a/app/src/main/java/ani/dantotsu/media/Studio.kt b/app/src/main/java/ani/dantotsu/media/Studio.kt index 699213b3..cc862fe7 100644 --- a/app/src/main/java/ani/dantotsu/media/Studio.kt +++ b/app/src/main/java/ani/dantotsu/media/Studio.kt @@ -5,5 +5,8 @@ import java.io.Serializable data class Studio( val id: String, val name: String, + val isFavourite: Boolean?, + val favourites: Int?, + val imageUrl: String?, var yearMedia: MutableMap>? = null ) : Serializable diff --git a/app/src/main/java/ani/dantotsu/media/StudioAdapter.kt b/app/src/main/java/ani/dantotsu/media/StudioAdapter.kt new file mode 100644 index 00000000..5e4f60b7 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/media/StudioAdapter.kt @@ -0,0 +1,61 @@ +package ani.dantotsu.media + +import android.app.Activity +import android.content.Intent +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.app.ActivityOptionsCompat +import androidx.core.content.ContextCompat +import androidx.core.util.Pair +import androidx.core.view.ViewCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import ani.dantotsu.copyToClipboard +import ani.dantotsu.databinding.ItemCharacterBinding +import ani.dantotsu.loadImage +import ani.dantotsu.setAnimation +import java.io.Serializable + +class StudioAdapter( + private val studioList: MutableList +) : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StudioViewHolder { + val binding = + ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return StudioViewHolder(binding) + } + + override fun onBindViewHolder(holder: StudioViewHolder, position: Int) { + val binding = holder.binding + setAnimation(binding.root.context, holder.binding.root) + val studio = studioList.getOrNull(position) ?: return + binding.itemCompactRelation.isVisible = false + binding.itemCompactImage.loadImage(studio.imageUrl) + binding.itemCompactTitle.text = studio.name + } + + override fun getItemCount(): Int = studioList.size + inner class StudioViewHolder(val binding: ItemCharacterBinding) : + RecyclerView.ViewHolder(binding.root) { + init { + itemView.setOnClickListener { + val studio = studioList[bindingAdapterPosition] + ContextCompat.startActivity( + itemView.context, + Intent( + itemView.context, + StudioActivity::class.java + ).putExtra("studio", studio as Serializable), + ActivityOptionsCompat.makeSceneTransitionAnimation( + itemView.context as Activity, + Pair.create( + binding.itemCompactImage, + ViewCompat.getTransitionName(binding.itemCompactImage)!! + ), + ).toBundle() + ) + } + itemView.setOnLongClickListener { copyToClipboard(studioList[bindingAdapterPosition].name ?: ""); true } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/SupportingSearchAdapter.kt b/app/src/main/java/ani/dantotsu/media/SupportingSearchAdapter.kt new file mode 100644 index 00000000..fdc8a05d --- /dev/null +++ b/app/src/main/java/ani/dantotsu/media/SupportingSearchAdapter.kt @@ -0,0 +1,142 @@ +package ani.dantotsu.media + +import android.annotation.SuppressLint +import android.graphics.drawable.Drawable +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.content.res.AppCompatResources +import androidx.recyclerview.widget.LinearLayoutManager +import ani.dantotsu.App.Companion.context +import ani.dantotsu.R +import ani.dantotsu.connections.anilist.AnilistSearch.SearchType +import ani.dantotsu.connections.anilist.AnilistSearch.SearchType.Companion.toAnilistString +import ani.dantotsu.connections.anilist.SearchResults +import ani.dantotsu.settings.saving.PrefManager +import ani.dantotsu.settings.saving.PrefName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class SupportingSearchAdapter(private val activity: SearchActivity, private val type: SearchType) : + HeaderInterface() { + + @SuppressLint("ClickableViewAccessibility") + override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) { + binding = holder.binding + + searchHistoryAdapter = SearchHistoryAdapter(type) { + binding.searchBarText.setText(it) + } + binding.searchHistoryList.layoutManager = LinearLayoutManager(binding.root.context) + binding.searchHistoryList.adapter = searchHistoryAdapter + + val imm: InputMethodManager = + activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager + + if (activity.searchType == SearchType.MANGA || activity.searchType == SearchType.ANIME) { + throw IllegalArgumentException("Invalid search type (wrong adapter)") + } + + binding.searchByImage.visibility = View.GONE + binding.searchResultGrid.visibility = View.GONE + binding.searchResultList.visibility = View.GONE + binding.searchFilter.visibility = View.GONE + binding.searchAdultCheck.visibility = View.GONE + binding.searchList.visibility = View.GONE + binding.searchChipRecycler.visibility = View.GONE + + binding.searchBar.hint = activity.searchType.toAnilistString() + if (PrefManager.getVal(PrefName.Incognito)) { + val startIconDrawableRes = R.drawable.ic_incognito_24 + val startIconDrawable: Drawable? = + context?.let { AppCompatResources.getDrawable(it, startIconDrawableRes) } + binding.searchBar.startIconDrawable = startIconDrawable + } + + binding.searchBarText.removeTextChangedListener(textWatcher) + when (type) { + SearchType.CHARACTER -> { + binding.searchBarText.setText(activity.characterResult.search) + } + + SearchType.STUDIO -> { + binding.searchBarText.setText(activity.studioResult.search) + } + + SearchType.STAFF -> { + binding.searchBarText.setText(activity.staffResult.search) + } + + SearchType.USER -> { + binding.searchBarText.setText(activity.userResult.search) + } + + else -> throw IllegalArgumentException("Invalid search type") + } + + binding.clearHistory.setOnClickListener { + it.startAnimation(fadeOutAnimation()) + it.visibility = View.GONE + searchHistoryAdapter.clearHistory() + } + updateClearHistoryVisibility() + fun searchTitle() { + val searchText = binding.searchBarText.text.toString().takeIf { it.isNotEmpty() } + + val result: SearchResults<*> = when (type) { + SearchType.CHARACTER -> activity.characterResult + SearchType.STUDIO -> activity.studioResult + SearchType.STAFF -> activity.staffResult + SearchType.USER -> activity.userResult + else -> throw IllegalArgumentException("Invalid search type") + } + + result.search = searchText + activity.search() + } + + textWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) {} + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + if (s.toString().isBlank()) { + activity.emptyMediaAdapter() + CoroutineScope(Dispatchers.IO).launch { + delay(200) + activity.runOnUiThread { + setHistoryVisibility(true) + } + } + } else { + setHistoryVisibility(false) + searchTitle() + } + } + } + binding.searchBarText.addTextChangedListener(textWatcher) + + binding.searchBarText.setOnEditorActionListener { _, actionId, _ -> + return@setOnEditorActionListener when (actionId) { + EditorInfo.IME_ACTION_SEARCH -> { + searchTitle() + binding.searchBarText.clearFocus() + imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0) + true + } + + else -> false + } + } + binding.searchBar.setEndIconOnClickListener { searchTitle() } + + search = Runnable { searchTitle() } + requestFocus = Runnable { binding.searchBarText.requestFocus() } + } +} diff --git a/app/src/main/java/ani/dantotsu/media/manga/Manga.kt b/app/src/main/java/ani/dantotsu/media/manga/Manga.kt index 24275add..636e9da5 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/Manga.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/Manga.kt @@ -5,7 +5,7 @@ import java.io.Serializable data class Manga( var totalChapters: Int? = null, - var selectedChapter: String? = null, + var selectedChapter: MangaChapter? = null, var chapters: MutableMap? = null, var slug: String? = null, var author: Author? = null, diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt index d8944b66..96cc4379 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt @@ -40,4 +40,6 @@ data class MangaChapter( private val dualPages = mutableListOf>() fun dualPages(): List> = dualPages + fun uniqueNumber(): String = "${number}-${scanlator ?: "Unknown"}" + } diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt index 933928e0..77f20b20 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt @@ -63,7 +63,7 @@ class MangaChapterAdapter( init { itemView.setOnClickListener { if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) - fragment.onMangaChapterClick(arr[bindingAdapterPosition].number) + fragment.onMangaChapterClick(arr[bindingAdapterPosition]) } } } @@ -74,7 +74,7 @@ class MangaChapterAdapter( fun startDownload(chapterNumber: String) { activeDownloads.add(chapterNumber) // Find the position of the chapter and notify only that item - val position = arr.indexOfFirst { it.number == chapterNumber } + val position = arr.indexOfFirst { it.uniqueNumber() == chapterNumber } if (position != -1) { notifyItemChanged(position) } @@ -84,17 +84,17 @@ class MangaChapterAdapter( activeDownloads.remove(chapterNumber) downloadedChapters.add(chapterNumber) // Find the position of the chapter and notify only that item - val position = arr.indexOfFirst { it.number == chapterNumber } + val position = arr.indexOfFirst { it.uniqueNumber() == chapterNumber } if (position != -1) { arr[position].progress = "Downloaded" notifyItemChanged(position) } } - fun deleteDownload(chapterNumber: String) { - downloadedChapters.remove(chapterNumber) + fun deleteDownload(chapterNumber: MangaChapter) { + downloadedChapters.remove(chapterNumber.uniqueNumber()) // Find the position of the chapter and notify only that item - val position = arr.indexOfFirst { it.number == chapterNumber } + val position = arr.indexOfFirst { it.uniqueNumber() == chapterNumber.uniqueNumber() } if (position != -1) { arr[position].progress = "" notifyItemChanged(position) @@ -105,7 +105,7 @@ class MangaChapterAdapter( activeDownloads.remove(chapterNumber) downloadedChapters.remove(chapterNumber) // Find the position of the chapter and notify only that item - val position = arr.indexOfFirst { it.number == chapterNumber } + val position = arr.indexOfFirst { it.uniqueNumber() == chapterNumber } if (position != -1) { arr[position].progress = "" notifyItemChanged(position) @@ -114,7 +114,7 @@ class MangaChapterAdapter( fun updateDownloadProgress(chapterNumber: String, progress: Int) { // Find the position of the chapter and notify only that item - val position = arr.indexOfFirst { it.number == chapterNumber } + val position = arr.indexOfFirst { it.uniqueNumber() == chapterNumber } if (position != -1) { arr[position].progress = "Downloading: ${progress}%" @@ -127,7 +127,8 @@ class MangaChapterAdapter( if (position < 0 || position >= arr.size) return for (i in 0.. { val binding = holder.binding val ep = arr[position] - holder.bind(ep.number, ep.progress) + holder.bind(ep.uniqueNumber(), ep.progress) setAnimation(fragment.requireContext(), holder.binding.root) binding.itemChapterNumber.text = ep.number diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt index 39aef576..9cebc4a6 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt @@ -422,12 +422,12 @@ class MangaReadAdapter( val startChapter = MediaNameAdapter.findChapterNumber(names[limit * (position)]) val endChapter = MediaNameAdapter.findChapterNumber(names[last - 1]) val startChapterString = if (startChapter != null) { - "Ch.$startChapter" + "Ch.%.1f".format(startChapter) } else { names[limit * (position)] } val endChapterString = if (endChapter != null) { - "Ch.$endChapter" + "Ch.%.1f".format(endChapter) } else { names[last - 1] } @@ -472,7 +472,6 @@ class MangaReadAdapter( val binding = _binding if (binding != null) { if (media.manga?.chapters != null) { - val chapters = media.manga.chapters!!.keys.toTypedArray() val anilistEp = (media.userProgress ?: 0).plus(1) val appEp = PrefManager.getNullableCustomVal( "${media.id}_current_chp", @@ -480,37 +479,39 @@ class MangaReadAdapter( String::class.java ) ?.toIntOrNull() ?: 1 - var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString() - val filteredChapters = chapters.filter { chapterKey -> - val chapter = media.manga.chapters!![chapterKey]!! - chapter.scanlator !in hiddenScanlators + val continueNumber = (if (anilistEp > appEp) anilistEp else appEp).toString() + val filteredChapters = media.manga.chapters!!.filter { chapter -> + if (mangaReadSources[media.selected!!.sourceIndex] is OfflineMangaParser) { + true + } else { + chapter.value.scanlator !in hiddenScanlators + } } val formattedChapters = filteredChapters.map { - MediaNameAdapter.findChapterNumber(it)?.toInt()?.toString() + MediaNameAdapter.findChapterNumber(it.value.number)?.toInt()?.toString() to it.key } - if (formattedChapters.contains(continueEp)) { - continueEp = chapters[formattedChapters.indexOf(continueEp)] + if (formattedChapters.any { it.first == continueNumber }) { + var continueEp = media.manga.chapters!![formattedChapters.first { it.first == continueNumber }.second] binding.sourceContinue.visibility = View.VISIBLE handleProgress( binding.itemMediaProgressCont, binding.itemMediaProgress, binding.itemMediaProgressEmpty, media.id, - continueEp + continueEp!!.number ) if ((binding.itemMediaProgress.layoutParams as LinearLayout.LayoutParams).weight > 0.8f) { - val e = chapters.indexOf(continueEp) - if (e != -1 && e + 1 < chapters.size) { - continueEp = chapters[e + 1] + val numberPlusOne = formattedChapters.indexOfFirst { it.first?.toIntOrNull() == continueNumber.toInt() + 1 } + if (numberPlusOne != -1) { + continueEp = media.manga.chapters!![formattedChapters[numberPlusOne].second] } } - val ep = media.manga.chapters!![continueEp]!! binding.itemMediaImage.loadImage(media.banner ?: media.cover) binding.mediaSourceContinueText.text = currActivity()!!.getString( R.string.continue_chapter, - ep.number, - if (!ep.title.isNullOrEmpty()) ep.title else "" + continueEp!!.number, + if (!continueEp.title.isNullOrEmpty()) continueEp.title else "" ) binding.sourceContinue.setOnClickListener { fragment.onMangaChapterClick(continueEp) diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt index be836ad5..b83da78e 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt @@ -52,6 +52,7 @@ import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.parsers.HMangaSources import ani.dantotsu.parsers.MangaParser import ani.dantotsu.parsers.MangaSources +import ani.dantotsu.parsers.OfflineMangaParser import ani.dantotsu.setNavigationTheme import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment import ani.dantotsu.settings.saving.PrefManager @@ -195,7 +196,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { for (download in downloadManager.mangaDownloadedTypes) { if (media.compareName(download.titleName)) { - chapterAdapter.stopDownload(download.chapterName) + chapterAdapter.stopDownload(download.uniqueName) } } @@ -249,7 +250,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { for (chapter in chaptersToDownload) { - onMangaChapterDownloadClick(chapter.title!!) + onMangaChapterDownloadClick(chapter) } } @@ -260,8 +261,12 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { val chapters = loadedChapters[media.selected!!.sourceIndex] if (chapters != null) { headerAdapter.options = getScanlators(chapters) - val filteredChapters = chapters.filterNot { (_, chapter) -> - chapter.scanlator in headerAdapter.hiddenScanlators + val filteredChapters = if (model.mangaReadSources?.get(media.selected!!.sourceIndex) is OfflineMangaParser) { + chapters + } else { + chapters.filterNot { (_, chapter) -> + chapter.scanlator in headerAdapter.hiddenScanlators + } } media.manga?.chapters = filteredChapters.toMutableMap() @@ -430,9 +435,9 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { } } - fun onMangaChapterClick(i: String) { + fun onMangaChapterClick(i: MangaChapter) { model.continueMedia = false - media.manga?.chapters?.get(i)?.let { + media.manga?.chapters?.get(i.uniqueNumber())?.let { media.manga?.selectedChapter = i model.saveSelected(media.id, media.selected!!) ChapterLoaderDialog.newInstance(it, true) @@ -440,7 +445,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { } } - fun onMangaChapterDownloadClick(i: String) { + fun onMangaChapterDownloadClick(i: MangaChapter) { activity?.let { if (!isNotificationPermissionGranted()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -453,7 +458,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { } fun continueDownload() { model.continueMedia = false - media.manga?.chapters?.get(i)?.let { chapter -> + media.manga?.chapters?.get(i.uniqueNumber())?.let { chapter -> val parser = model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser parser?.let { @@ -464,6 +469,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { val downloadTask = MangaDownloaderService.DownloadTask( title = media.mainName(), chapter = chapter.title!!, + scanlator = chapter.scanlator ?: "Unknown", imageData = images, sourceMedia = media, retries = 2, @@ -483,7 +489,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { // Inform the adapter that the download has started withContext(Dispatchers.Main) { - chapterAdapter.startDownload(i) + chapterAdapter.startDownload(i.uniqueNumber()) } } } @@ -514,11 +520,11 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { } - fun onMangaChapterRemoveDownloadClick(i: String) { + fun onMangaChapterRemoveDownloadClick(i: MangaChapter) { downloadManager.removeDownload( DownloadedType( media.mainName(), - i, + i.number, MediaType.MANGA ) ) { @@ -526,7 +532,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { } } - fun onMangaChapterStopDownloadClick(i: String) { + fun onMangaChapterStopDownloadClick(i: MangaChapter) { val cancelIntent = Intent().apply { action = MangaDownloaderService.ACTION_CANCEL_DOWNLOAD putExtra(MangaDownloaderService.EXTRA_CHAPTER, i) @@ -537,11 +543,11 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { downloadManager.removeDownload( DownloadedType( media.mainName(), - i, + i.number, MediaType.MANGA ) ) { - chapterAdapter.purgeDownload(i) + chapterAdapter.purgeDownload(i.uniqueNumber()) } } @@ -584,7 +590,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { // Find latest chapter for subscription selected.latest = - media.manga?.chapters?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f + media.manga?.chapters?.values?.maxOfOrNull { MediaNameAdapter.findChapterNumber(it.number) ?: 0f } ?: 0f selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt index 689ab6af..89597a7c 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt @@ -196,7 +196,7 @@ class MangaReaderActivity : AppCompatActivity() { finish() return@addCallback } - val chapter = (MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!) + val chapter = (MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!.number) ?.minus(1L) ?: 0).toString() if (chapter == "0.0" && PrefManager.getVal(PrefName.ChapterZeroReader) // Not asking individually or incognito @@ -279,7 +279,7 @@ class MangaReaderActivity : AppCompatActivity() { defaultSettings = loadReaderSettings("${media.id}_current_settings") ?: defaultSettings chapters = media.manga?.chapters ?: return - chapter = chapters[media.manga!!.selectedChapter] ?: return + chapter = chapters[media.manga!!.selectedChapter!!.uniqueNumber()] ?: return model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources binding.mangaReaderSource.isVisible = PrefManager.getVal(PrefName.ShowSource) @@ -309,7 +309,7 @@ class MangaReaderActivity : AppCompatActivity() { binding.mangaReaderTitle.text = media.userPreferredName chaptersArr = chapters.keys.toList() - currentChapterIndex = chaptersArr.indexOf(media.manga!!.selectedChapter) + currentChapterIndex = chaptersArr.indexOf(media.manga!!.selectedChapter!!.uniqueNumber()) chaptersTitleArr = arrayListOf() chapters.forEach { @@ -394,10 +394,10 @@ class MangaReaderActivity : AppCompatActivity() { model.getMangaChapter().observe(this) { chap -> if (chap != null) { chapter = chap - media.manga!!.selectedChapter = chapter.number + media.manga!!.selectedChapter = chapter media.selected = model.loadSelected(media) PrefManager.setCustomVal("${media.id}_current_chp", chap.number) - currentChapterIndex = chaptersArr.indexOf(chap.number) + currentChapterIndex = chaptersArr.indexOf(chap.uniqueNumber()) binding.mangaReaderChapterSelect.setSelection(currentChapterIndex) if (directionRLBT) { binding.mangaReaderNextChap.text = @@ -1036,7 +1036,7 @@ class MangaReaderActivity : AppCompatActivity() { PrefManager.setCustomVal("${media.id}_save_progress", true) updateProgress( media, - MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!) + MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!.number) .toString() ) runnable.run() @@ -1057,7 +1057,7 @@ class MangaReaderActivity : AppCompatActivity() { ) updateProgress( media, - MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!) + MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!.number) .toString() ) runnable.run() diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt index 0db36d74..adfd857e 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt @@ -499,7 +499,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { sChapter.url, sChapter.name, null, - sChapter.scanlator, + sChapter.scanlator ?: "Unknown", sChapter, sChapter.date_upload ) diff --git a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt index abfe0f8a..948ba2c3 100644 --- a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt @@ -1,3 +1,4 @@ + package ani.dantotsu.parsers import android.graphics.drawable.Drawable diff --git a/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt b/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt index 1dd000b8..aeae9f67 100644 --- a/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt +++ b/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt @@ -90,7 +90,7 @@ abstract class MangaReadSources : BaseSources() { show.sManga?.let { sManga -> tryWithSuspend(true) { parser.loadChapters(show.link, show.extra, sManga).forEach { - map[it.number] = MangaChapter(it) + map["${it.number}-${it.scanlator}"] = MangaChapter(it) } } } @@ -102,7 +102,7 @@ abstract class MangaReadSources : BaseSources() { tryWithSuspend(true) { // Since we've checked, we can safely cast parser to OfflineMangaParser and call its methods parser.loadChapters(show.link, show.extra, SManga.create()).forEach { - map[it.number] = MangaChapter(it) + map["${it.number}-${it.scanlator}"] = MangaChapter(it) } } } else { diff --git a/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt b/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt index bf67a8ca..266893f8 100644 --- a/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt @@ -79,7 +79,7 @@ data class MangaChapter( //Self-Descriptive val title: String? = null, val description: String? = null, - val scanlator: String? = null, + val scanlator: String, val sChapter: SChapter, val date: Long? = null, ) diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt index ec3ca338..eb8a8bda 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt @@ -31,13 +31,17 @@ class OfflineMangaParser : MangaParser() { val chapters = mutableListOf() if (directory?.exists() == true) { directory.listFiles().forEach { + val scanlator = downloadManager.mangaDownloadedTypes.find { items -> + items.titleName == mangaLink && + items.chapterName == it.name + }?.scanlator ?: "Unknown" if (it.isDirectory) { val chapter = MangaChapter( it.name!!, "$mangaLink/${it.name}", it.name, null, - null, + scanlator, SChapter.create() ) chapters.add(chapter) @@ -45,8 +49,7 @@ class OfflineMangaParser : MangaParser() { } } chapters.addAll(loadChaptersCompat(mangaLink, extra, sManga)) - return chapters.distinctBy { it.number } - .sortedBy { MediaNameAdapter.findChapterNumber(it.number) } + return chapters.sortedBy { MediaNameAdapter.findChapterNumber(it.number) } } override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List { diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtension.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtension.kt index 42595aaf..3261ca77 100644 --- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtension.kt +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtension.kt @@ -26,6 +26,7 @@ sealed class NovelExtension { override val pkgName: String, override val versionName: String, override val versionCode: Long, + var repository: String, val sources: List, val iconUrl: String, ) : NovelExtension() diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt deleted file mode 100644 index 0a225c9d..00000000 --- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt +++ /dev/null @@ -1,186 +0,0 @@ -package ani.dantotsu.parsers.novel - - -import android.content.Context -import ani.dantotsu.settings.saving.PrefManager -import ani.dantotsu.settings.saving.PrefName -import ani.dantotsu.util.Logger -import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier -import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension -import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult -import eu.kanade.tachiyomi.extension.util.ExtensionLoader -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.network.awaitSuccess -import eu.kanade.tachiyomi.network.parseAs -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import tachiyomi.core.util.lang.withIOContext -import uy.kohesive.injekt.injectLazy -import java.util.Date -import kotlin.time.Duration.Companion.days - -class NovelExtensionGithubApi { - - private val networkService: NetworkHelper by injectLazy() - private val novelExtensionManager: NovelExtensionManager by injectLazy() - private val json: Json by injectLazy() - - private val lastExtCheck: Long = PrefManager.getVal(PrefName.NovelLastExtCheck) - - private var requiresFallbackSource = false - - suspend fun findExtensions(): List { - return withIOContext { - val githubResponse = if (requiresFallbackSource) { - null - } else { - try { - networkService.client - .newCall(GET("${REPO_URL_PREFIX}index.min.json")) - .awaitSuccess() - } catch (e: Throwable) { - Logger.log("Failed to get extensions from GitHub") - requiresFallbackSource = true - null - } - } - - val response = githubResponse ?: run { - Logger.log("using fallback source") - networkService.client - .newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json")) - .awaitSuccess() - } - - Logger.log("response: $response") - - val extensions = with(json) { - response - .parseAs>() - .toExtensions() - } - - // Sanity check - a small number of extensions probably means something broke - // with the repo generator - /*if (extensions.size < 10) { //TODO: uncomment when more extensions are added - throw Exception() - }*/ - Logger.log("extensions: $extensions") - extensions - } - } - - suspend fun checkForUpdates( - context: Context, - fromAvailableExtensionList: Boolean = false - ): List? { - // Limit checks to once a day at most - if (fromAvailableExtensionList && Date().time < lastExtCheck + 1.days.inWholeMilliseconds) { - return null - } - - val extensions = if (fromAvailableExtensionList) { - novelExtensionManager.availableExtensionsFlow.value - } else { - findExtensions().also { - PrefManager.setVal(PrefName.NovelLastExtCheck, Date().time) - } - } - - val installedExtensions = ExtensionLoader.loadNovelExtensions(context) - .filterIsInstance() - .map { it.extension } - - val extensionsWithUpdate = mutableListOf() - for (installedExt in installedExtensions) { - val pkgName = installedExt.pkgName - val availableExt = extensions.find { it.pkgName == pkgName } ?: continue - - val hasUpdatedVer = availableExt.versionCode > installedExt.versionCode - val hasUpdate = installedExt.isUnofficial.not() && (hasUpdatedVer) - if (hasUpdate) { - extensionsWithUpdate.add(installedExt) - } - } - - if (extensionsWithUpdate.isNotEmpty()) { - ExtensionUpdateNotifier(context).promptUpdates(extensionsWithUpdate.map { it.name }) - } - - return extensionsWithUpdate - } - - private fun List.toExtensions(): List { - return mapNotNull { extension -> - val sources = extension.sources?.map { source -> - NovelExtensionSourceJsonObject( - source.id, - source.lang, - source.name, - source.baseUrl, - ) - } - val iconUrl = "${REPO_URL_PREFIX}icon/${extension.pkg}.png" - NovelExtension.Available( - extension.name, - extension.pkg, - extension.apk, - extension.code, - sources?.toSources() ?: emptyList(), - iconUrl, - ) - } - } - - private fun List.toSources(): List { - return map { source -> - AvailableNovelSources( - source.id, - source.lang, - source.name, - source.baseUrl, - ) - } - } - - fun getApkUrl(extension: NovelExtension.Available): String { - return "${getUrlPrefix()}apk/${extension.pkgName}.apk" - } - - private fun getUrlPrefix(): String { - return if (requiresFallbackSource) { - FALLBACK_REPO_URL_PREFIX - } else { - REPO_URL_PREFIX - } - } -} - -private const val REPO_URL_PREFIX = - "https://raw.githubusercontent.com/dannovels/novel-extensions/main/" -private const val FALLBACK_REPO_URL_PREFIX = - "https://gcore.jsdelivr.net/gh/dannovels/novel-extensions@latest/" - -@Serializable -private data class NovelExtensionJsonObject( - val name: String, - val pkg: String, - val apk: String, - val lang: String, - val code: Long, - val version: String, - val nsfw: Int, - val hasReadme: Int = 0, - val hasChangelog: Int = 0, - val sources: List?, -) - -@Serializable -private data class NovelExtensionSourceJsonObject( - val id: Long, - val lang: String, - val name: String, - val baseUrl: String, -) - diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt index aab307b4..145fd3b2 100644 --- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt @@ -6,6 +6,7 @@ import ani.dantotsu.media.MediaType import ani.dantotsu.snackString import ani.dantotsu.util.Logger import eu.kanade.tachiyomi.extension.InstallStep +import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver import eu.kanade.tachiyomi.extension.util.ExtensionInstaller import eu.kanade.tachiyomi.extension.util.ExtensionLoader @@ -22,7 +23,7 @@ class NovelExtensionManager(private val context: Context) { /** * API where all the available Novel extensions can be found. */ - private val api = NovelExtensionGithubApi() + private val api = ExtensionGithubApi() /** * The installer which installs, updates and uninstalls the Novel extensions. @@ -70,7 +71,7 @@ class NovelExtensionManager(private val context: Context) { */ suspend fun findAvailableExtensions() { val extensions: List = try { - api.findExtensions() + api.findNovelExtensions() } catch (e: Exception) { Logger.log("Error finding extensions: ${e.message}") withUIContext { snackString("Failed to get Novel extensions list") } @@ -119,7 +120,7 @@ class NovelExtensionManager(private val context: Context) { * @param extension The anime extension to be installed. */ fun installExtension(extension: NovelExtension.Available): Observable { - return installer.downloadAndInstall(api.getApkUrl(extension), extension.pkgName, + return installer.downloadAndInstall(api.getNovelApkUrl(extension), extension.pkgName, extension.name, MediaType.NOVEL) } @@ -233,7 +234,7 @@ class NovelExtensionManager(private val context: Context) { private fun NovelExtension.Installed.updateExists(availableNovelExtension: NovelExtension.Available? = null): Boolean { val availableExt = availableNovelExtension ?: _availableNovelExtensionsFlow.value.find { it.pkgName == pkgName } - if (isUnofficial || availableExt == null) return false + if (availableExt == null) return false return (availableExt.versionCode > versionCode) } diff --git a/app/src/main/java/ani/dantotsu/profile/UsersAdapter.kt b/app/src/main/java/ani/dantotsu/profile/UsersAdapter.kt index 90363e76..7651d21f 100644 --- a/app/src/main/java/ani/dantotsu/profile/UsersAdapter.kt +++ b/app/src/main/java/ani/dantotsu/profile/UsersAdapter.kt @@ -4,16 +4,19 @@ import android.content.Intent import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding import ani.dantotsu.databinding.ItemFollowerBinding +import ani.dantotsu.databinding.ItemFollowerGridBinding import ani.dantotsu.loadImage import ani.dantotsu.setAnimation -class UsersAdapter(private val user: ArrayList) : +class UsersAdapter(private val user: MutableList, private val grid: Boolean = false) : RecyclerView.Adapter() { - inner class UsersViewHolder(val binding: ItemFollowerBinding) : + inner class UsersViewHolder(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) { init { itemView.setOnClickListener { @@ -27,6 +30,11 @@ class UsersAdapter(private val user: ArrayList) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UsersViewHolder { return UsersViewHolder( + if (grid) ItemFollowerGridBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) else ItemFollowerBinding.inflate( LayoutInflater.from(parent.context), parent, @@ -36,12 +44,21 @@ class UsersAdapter(private val user: ArrayList) : } override fun onBindViewHolder(holder: UsersViewHolder, position: Int) { - val b = holder.binding - setAnimation(b.root.context, b.root) - val user = user[position] - b.profileUserAvatar.loadImage(user.pfp) - b.profileBannerImage.loadImage(user.banner ?: user.pfp) - b.profileUserName.text = user.name + setAnimation(holder.binding.root.context, holder.binding.root) + val user = user.getOrNull(position) ?: return + if (grid) { + val b = holder.binding as ItemFollowerGridBinding + b.profileUserAvatar.loadImage(user.pfp) + b.profileUserName.text = user.name + b.profileCompactScoreBG.isVisible = false + b.profileInfo.isVisible = false + b.profileCompactProgressContainer.isVisible = false + } else { + val b = holder.binding as ItemFollowerBinding + b.profileUserAvatar.loadImage(user.pfp) + b.profileBannerImage.loadImage(user.banner ?: user.pfp) + b.profileUserName.text = user.name + } } override fun getItemCount(): Int = user.size diff --git a/app/src/main/java/ani/dantotsu/settings/AddRepositoryBottomSheet.kt b/app/src/main/java/ani/dantotsu/settings/AddRepositoryBottomSheet.kt index 4581f8cb..30dbdaba 100644 --- a/app/src/main/java/ani/dantotsu/settings/AddRepositoryBottomSheet.kt +++ b/app/src/main/java/ani/dantotsu/settings/AddRepositoryBottomSheet.kt @@ -2,6 +2,7 @@ package ani.dantotsu.settings import android.content.Context import android.os.Bundle +import android.view.HapticFeedbackConstants import android.view.KeyEvent import android.view.LayoutInflater import android.view.View @@ -10,29 +11,52 @@ import android.view.inputmethod.EditorInfo import androidx.recyclerview.widget.LinearLayoutManager import ani.dantotsu.BottomSheetDialogFragment import ani.dantotsu.R +import ani.dantotsu.copyToClipboard import ani.dantotsu.databinding.BottomSheetAddRepositoryBinding import ani.dantotsu.databinding.ItemRepoBinding import ani.dantotsu.media.MediaType +import ani.dantotsu.parsers.novel.NovelExtensionManager +import ani.dantotsu.settings.saving.PrefManager +import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.util.customAlertDialog import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.viewbinding.BindableItem +import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager +import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get class RepoItem( val url: String, - val onRemove: (String) -> Unit + private val mediaType: MediaType, + val onRemove: (String, MediaType) -> Unit ) :BindableItem() { override fun getLayout() = R.layout.item_repo override fun bind(viewBinding: ItemRepoBinding, position: Int) { - viewBinding.repoNameTextView.text = url + viewBinding.repoNameTextView.text = url.cleanShownUrl() viewBinding.repoDeleteImageView.setOnClickListener { - onRemove(url) + onRemove(url, mediaType) + } + viewBinding.repoCopyImageView.setOnClickListener { + viewBinding.repoCopyImageView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + copyToClipboard(url, true) } } override fun initializeViewBinding(view: View): ItemRepoBinding { return ItemRepoBinding.bind(view) } + + private fun String.cleanShownUrl(): String { + return this + .removePrefix("https://raw.githubusercontent.com/") + .replace("index.min.json", "") + .removeSuffix("/") + } } class AddRepositoryBottomSheet : BottomSheetDialogFragment() { @@ -41,7 +65,7 @@ class AddRepositoryBottomSheet : BottomSheetDialogFragment() { private var mediaType: MediaType = MediaType.ANIME private var onRepositoryAdded: ((String, MediaType) -> Unit)? = null private var repositories: MutableList = mutableListOf() - private var onRepositoryRemoved: ((String) -> Unit)? = null + private var onRepositoryRemoved: ((String, MediaType) -> Unit)? = null private var adapter: GroupieAdapter = GroupieAdapter() override fun onCreateView( @@ -62,24 +86,19 @@ class AddRepositoryBottomSheet : BottomSheetDialogFragment() { LinearLayoutManager.VERTICAL, false ) - adapter.addAll(repositories.map { RepoItem(it, ::onRepositoryRemoved) }) + adapter.addAll(repositories.map { RepoItem(it, mediaType, ::onRepositoryRemoved) }) binding.repositoryInput.hint = when(mediaType) { MediaType.ANIME -> getString(R.string.anime_add_repository) MediaType.MANGA -> getString(R.string.manga_add_repository) - else -> "" + MediaType.NOVEL -> getString(R.string.novel_add_repository) } binding.addButton.setOnClickListener { val input = binding.repositoryInput.text.toString() val error = isValidUrl(input) if (error == null) { - context?.let { context -> - addRepoWarning(context) { - onRepositoryAdded?.invoke(input, mediaType) - dismiss() - } - } + acceptUrl(input) } else { binding.repositoryInput.error = error } @@ -96,12 +115,7 @@ class AddRepositoryBottomSheet : BottomSheetDialogFragment() { if (url.isNotBlank()) { val error = isValidUrl(url) if (error == null) { - context?.let { context -> - addRepoWarning(context) { - onRepositoryAdded?.invoke(url, mediaType) - dismiss() - } - } + acceptUrl(url) return@setOnEditorActionListener true } else { binding.repositoryInput.error = error @@ -112,20 +126,62 @@ class AddRepositoryBottomSheet : BottomSheetDialogFragment() { } } - private fun onRepositoryRemoved(url: String) { - onRepositoryRemoved?.invoke(url) - repositories.remove(url) - adapter.update(repositories.map { RepoItem(it, ::onRepositoryRemoved) }) + private fun acceptUrl(url: String) { + val finalUrl = getRepoUrl(url) + context?.let { context -> + addRepoWarning(context) { + onRepositoryAdded?.invoke(finalUrl, mediaType) + dismiss() + } + } } - private fun isValidUrl(url: String): String? { - if (!url.startsWith("https://") && !url.startsWith("http://")) - return "URL must start with http:// or https://" - if (!url.removeSuffix("/").endsWith("index.min.json")) - return "URL must end with index.min.json" + private fun isValidUrl(input: String): String? { + if (input.startsWith("http://") || input.startsWith("https://")) { + if (!input.removeSuffix("/").endsWith("index.min.json")) { + return "URL must end with index.min.json" + } + return null + } + + val parts = input.split("/") + if (parts.size !in 2..3) { + return "Must be a full URL or in format: username/repo[/branch]" + } + + val username = parts[0] + val repo = parts[1] + val branch = if (parts.size == 3) parts[2] else "repo" + + if (username.isBlank() || repo.isBlank()) { + return "Username and repository name cannot be empty" + } + if (parts.size == 3 && branch.isBlank()) { + return "Branch name cannot be empty" + } + return null } + private fun getRepoUrl(input: String): String { + if (input.startsWith("http://") || input.startsWith("https://")) { + return input + } + + val parts = input.split("/") + val username = parts[0] + val repo = parts[1] + val branch = if (parts.size == 3) parts[2] else "repo" + + return "https://raw.githubusercontent.com/$username/$repo/$branch/index.min.json" + } + + private fun onRepositoryRemoved(url: String, mediaType: MediaType) { + onRepositoryRemoved?.invoke(url, mediaType) + repositories.remove(url) + adapter.update(repositories.map { RepoItem(it, mediaType, ::onRepositoryRemoved) }) + } + override fun onDestroyView() { super.onDestroyView() _binding = null @@ -142,11 +198,81 @@ class AddRepositoryBottomSheet : BottomSheetDialogFragment() { .setNegButton(R.string.cancel) { } .show() } + + fun addRepo(input: String, mediaType: MediaType) { + val validLink = if (input.contains("github.com") && input.contains("blob")) { + input.replace("github.com", "raw.githubusercontent.com") + .replace("/blob/", "/") + } else input + + when (mediaType) { + MediaType.ANIME -> { + val anime = + PrefManager.getVal>(PrefName.AnimeExtensionRepos) + .plus(validLink) + PrefManager.setVal(PrefName.AnimeExtensionRepos, anime) + CoroutineScope(Dispatchers.IO).launch { + Injekt.get().findAvailableExtensions() + } + } + MediaType.MANGA -> { + val manga = + PrefManager.getVal>(PrefName.MangaExtensionRepos) + .plus(validLink) + PrefManager.setVal(PrefName.MangaExtensionRepos, manga) + CoroutineScope(Dispatchers.IO).launch { + Injekt.get().findAvailableExtensions() + } + } + MediaType.NOVEL -> { + val novel = + PrefManager.getVal>(PrefName.NovelExtensionRepos) + .plus(validLink) + PrefManager.setVal(PrefName.NovelExtensionRepos, novel) + CoroutineScope(Dispatchers.IO).launch { + Injekt.get().findAvailableExtensions() + } + } + } + } + + fun removeRepo(input: String, mediaType: MediaType) { + when (mediaType) { + MediaType.ANIME -> { + val anime = + PrefManager.getVal>(PrefName.AnimeExtensionRepos) + .minus(input) + PrefManager.setVal(PrefName.AnimeExtensionRepos, anime) + CoroutineScope(Dispatchers.IO).launch { + Injekt.get().findAvailableExtensions() + } + } + MediaType.MANGA -> { + val manga = + PrefManager.getVal>(PrefName.MangaExtensionRepos) + .minus(input) + PrefManager.setVal(PrefName.MangaExtensionRepos, manga) + CoroutineScope(Dispatchers.IO).launch { + Injekt.get().findAvailableExtensions() + } + } + MediaType.NOVEL -> { + val novel = + PrefManager.getVal>(PrefName.NovelExtensionRepos) + .minus(input) + PrefManager.setVal(PrefName.NovelExtensionRepos, novel) + CoroutineScope(Dispatchers.IO).launch { + Injekt.get().findAvailableExtensions() + } + } + } + } + fun newInstance( mediaType: MediaType, repositories: List, onRepositoryAdded: (String, MediaType) -> Unit, - onRepositoryRemoved: (String) -> Unit + onRepositoryRemoved: (String, MediaType) -> Unit ): AddRepositoryBottomSheet { return AddRepositoryBottomSheet().apply { this.mediaType = mediaType diff --git a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt index dd11df3a..de9dbbf0 100644 --- a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt @@ -1,18 +1,12 @@ package ani.dantotsu.settings -import android.app.AlertDialog import android.content.Intent import android.os.Bundle import android.text.Editable import android.text.TextWatcher -import android.view.HapticFeedbackConstants -import android.view.KeyEvent -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.inputmethod.EditorInfo import android.widget.AutoCompleteTextView -import android.widget.EditText import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.view.updateLayoutParams @@ -20,10 +14,7 @@ import androidx.fragment.app.Fragment import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.R -import ani.dantotsu.copyToClipboard import ani.dantotsu.databinding.ActivityExtensionsBinding -import ani.dantotsu.databinding.DialogRepositoriesBinding -import ani.dantotsu.databinding.ItemRepositoryBinding import ani.dantotsu.initActivity import ani.dantotsu.media.MediaType import ani.dantotsu.navBarHeight @@ -37,20 +28,11 @@ import ani.dantotsu.themes.ThemeManager import ani.dantotsu.util.customAlertDialog import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator -import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager -import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import uy.kohesive.injekt.injectLazy import java.util.Locale class ExtensionsActivity : AppCompatActivity() { lateinit var binding: ActivityExtensionsBinding - private val animeExtensionManager: AnimeExtensionManager by injectLazy() - private val mangaExtensionManager: MangaExtensionManager by injectLazy() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -124,6 +106,9 @@ class ExtensionsActivity : AppCompatActivity() { if (tab.text?.contains("Manga") == true) { generateRepositoryButton(MediaType.MANGA) } + if (tab.text?.contains("Novels") == true) { + generateRepositoryButton(MediaType.NOVEL) + } } override fun onTabUnselected(tab: TabLayout.Tab) { @@ -199,136 +184,28 @@ class ExtensionsActivity : AppCompatActivity() { } } - private fun processUserInput(input: String, mediaType: MediaType) { - val entry = if (input.endsWith("/") || input.endsWith("index.min.json")) - input.substring(0, input.lastIndexOf("/")) else input - if (mediaType == MediaType.ANIME) { - val anime = - PrefManager.getVal>(PrefName.AnimeExtensionRepos).plus(entry) - PrefManager.setVal(PrefName.AnimeExtensionRepos, anime) - CoroutineScope(Dispatchers.IO).launch { - animeExtensionManager.findAvailableExtensions() - } - } - if (mediaType == MediaType.MANGA) { - val manga = - PrefManager.getVal>(PrefName.MangaExtensionRepos).plus(entry) - PrefManager.setVal(PrefName.MangaExtensionRepos, manga) - CoroutineScope(Dispatchers.IO).launch { - mangaExtensionManager.findAvailableExtensions() - } - } - } - - private fun getSavedRepositories(repoInventory: ViewGroup, type: MediaType) { - repoInventory.removeAllViews() - val prefName: PrefName? = when (type) { - MediaType.ANIME -> { - PrefName.AnimeExtensionRepos - } - - MediaType.MANGA -> { - PrefName.MangaExtensionRepos - } - - else -> { - null - } - } - prefName?.let { repoList -> - PrefManager.getVal>(repoList).forEach { item -> - val view = ItemRepositoryBinding.inflate( - LayoutInflater.from(repoInventory.context), repoInventory, true - ) - view.repositoryItem.text = item.removePrefix("https://raw.githubusercontent.com") - view.repositoryItem.setOnClickListener { - customAlertDialog().apply { - setTitle(R.string.rem_repository) - setMessage(item) - setPosButton(R.string.ok) { - val repos = PrefManager.getVal>(prefName).minus(item) - PrefManager.setVal(prefName, repos) - repoInventory.removeView(view.root) - CoroutineScope(Dispatchers.IO).launch { - when (type) { - MediaType.ANIME -> { - animeExtensionManager.findAvailableExtensions() - } - - MediaType.MANGA -> { - mangaExtensionManager.findAvailableExtensions() - } - - else -> {} - } - } - } - setNegButton(R.string.cancel) - show() - } - } - view.repositoryItem.setOnLongClickListener { - it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - copyToClipboard(item, true) - true - } - } - } - } - - private fun processEditorAction(editText: EditText, mediaType: MediaType) { - editText.setOnEditorActionListener { textView, action, keyEvent -> - if (action == EditorInfo.IME_ACTION_SEARCH || action == EditorInfo.IME_ACTION_DONE || - (keyEvent?.action == KeyEvent.ACTION_UP - && keyEvent.keyCode == KeyEvent.KEYCODE_ENTER) - ) { - return@setOnEditorActionListener if (textView.text.isNullOrBlank()) { - false - } else { - processUserInput(textView.text.toString(), mediaType) - true - } - } - false - } - } - private fun generateRepositoryButton(type: MediaType) { - val hintResource: Int? = when (type) { - MediaType.ANIME -> { - R.string.anime_add_repository - } - - MediaType.MANGA -> { - R.string.manga_add_repository - } - - else -> { - null - } - } - hintResource?.let { res -> - binding.openSettingsButton.setOnClickListener { - val dialogView = DialogRepositoriesBinding.inflate( - LayoutInflater.from(binding.openSettingsButton.context), null, false - ) - dialogView.repositoryTextBox.hint = getString(res) - dialogView.repoInventory.apply { - getSavedRepositories(this, type) + binding.openSettingsButton.setOnClickListener { + val repos: Set = when (type) { + MediaType.ANIME -> { + PrefManager.getVal(PrefName.AnimeExtensionRepos) } - processEditorAction(dialogView.repositoryTextBox, type) - customAlertDialog().apply { - setTitle(R.string.edit_repositories) - setCustomView(dialogView.root) - setPosButton(R.string.add_list) { - if (!dialogView.repositoryTextBox.text.isNullOrBlank()) { - processUserInput(dialogView.repositoryTextBox.text.toString(), type) - } - } - setNegButton(R.string.close) - show() + + MediaType.MANGA -> { + PrefManager.getVal(PrefName.MangaExtensionRepos) + } + + MediaType.NOVEL -> { + PrefManager.getVal(PrefName.NovelExtensionRepos) } } + AddRepositoryBottomSheet.newInstance( + type, + repos.toList(), + AddRepositoryBottomSheet::addRepo, + AddRepositoryBottomSheet::removeRepo + + ).show(supportFragmentManager, "add_repo") } } } diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt index dea8666c..51dfa89e 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt @@ -155,6 +155,16 @@ class SettingsCommonActivity : AppCompatActivity() { }, isActivity = true ), + Settings( + type = 2, + name = getString(R.string.open_animanga_directly), + desc = getString(R.string.open_animanga_directly_info), + icon = R.drawable.ic_round_search_24, + isChecked = PrefManager.getVal(PrefName.AniMangaSearchDirect), + switch = { isChecked, _ -> + PrefManager.setVal(PrefName.AniMangaSearchDirect, isChecked) + } + ), Settings( type = 1, name = getString(R.string.download_manager_select), diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt index 59add578..8a84d53d 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt @@ -1,14 +1,10 @@ package ani.dantotsu.settings -import android.app.AlertDialog import android.content.Intent import android.os.Bundle import android.view.HapticFeedbackConstants -import android.view.KeyEvent import android.view.LayoutInflater import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.widget.EditText import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.view.isVisible @@ -32,9 +28,6 @@ import ani.dantotsu.util.customAlertDialog import eu.kanade.domain.base.BasePreferences import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy @@ -42,8 +35,7 @@ import uy.kohesive.injekt.injectLazy class SettingsExtensionsActivity : AppCompatActivity() { private lateinit var binding: ActivitySettingsExtensionsBinding private val extensionInstaller = Injekt.get().extensionInstaller() - private val animeExtensionManager: AnimeExtensionManager by injectLazy() - private val mangaExtensionManager: MangaExtensionManager by injectLazy() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ThemeManager(this).applyTheme() @@ -61,7 +53,7 @@ class SettingsExtensionsActivity : AppCompatActivity() { } fun setExtensionOutput(repoInventory: ViewGroup, type: MediaType) { repoInventory.removeAllViews() - val prefName: PrefName? = when (type) { + val prefName: PrefName = when (type) { MediaType.ANIME -> { PrefName.AnimeExtensionRepos } @@ -70,74 +62,24 @@ class SettingsExtensionsActivity : AppCompatActivity() { PrefName.MangaExtensionRepos } - else -> { - null + MediaType.NOVEL -> { + PrefName.NovelExtensionRepos } } - prefName?.let { repoList -> - PrefManager.getVal>(repoList).forEach { item -> - val view = ItemRepositoryBinding.inflate( - LayoutInflater.from(repoInventory.context), repoInventory, true - ) - view.repositoryItem.text = - item.removePrefix("https://raw.githubusercontent.com/") - view.repositoryItem.setOnClickListener { - context.customAlertDialog().apply { - setTitle(R.string.rem_repository) - setMessage(item) - setPosButton(R.string.ok) { - val repos = PrefManager.getVal>(repoList).minus(item) - PrefManager.setVal(repoList, repos) - setExtensionOutput(repoInventory, type) - CoroutineScope(Dispatchers.IO).launch { - when (type) { - MediaType.ANIME -> { - animeExtensionManager.findAvailableExtensions() - } - MediaType.MANGA -> { - mangaExtensionManager.findAvailableExtensions() - } - else -> {} - } - } - } - setNegButton(R.string.cancel) - show() - } - } - view.repositoryItem.setOnLongClickListener { - it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - copyToClipboard(item, true) - true - } - } - repoInventory.isVisible = repoInventory.childCount > 0 - } - } + PrefManager.getVal>(prefName).forEach { item -> + val view = ItemRepositoryBinding.inflate( + LayoutInflater.from(repoInventory.context), repoInventory, true + ) + view.repositoryItem.text = + item.removePrefix("https://raw.githubusercontent.com/") - fun processUserInput(input: String, mediaType: MediaType, view: ViewGroup) { - val validLink = if (input.contains("github.com") && input.contains("blob")) { - input.replace("github.com", "raw.githubusercontent.com") - .replace("/blob/", "/") - } else input - if (mediaType == MediaType.ANIME) { - val anime = - PrefManager.getVal>(PrefName.AnimeExtensionRepos).plus(validLink) - PrefManager.setVal(PrefName.AnimeExtensionRepos, anime) - CoroutineScope(Dispatchers.IO).launch { - animeExtensionManager.findAvailableExtensions() + view.repositoryItem.setOnLongClickListener { + it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + copyToClipboard(item, true) + true } - setExtensionOutput(view, MediaType.ANIME) - } - if (mediaType == MediaType.MANGA) { - val manga = - PrefManager.getVal>(PrefName.MangaExtensionRepos).plus(validLink) - PrefManager.setVal(PrefName.MangaExtensionRepos, manga) - CoroutineScope(Dispatchers.IO).launch { - mangaExtensionManager.findAvailableExtensions() - } - setExtensionOutput(view, MediaType.MANGA) } + repoInventory.isVisible = repoInventory.childCount > 0 } settingsRecyclerView.adapter = SettingsAdapter( @@ -148,17 +90,18 @@ class SettingsExtensionsActivity : AppCompatActivity() { desc = getString(R.string.anime_add_repository_desc), icon = R.drawable.ic_github, onClick = { - val animeRepos = PrefManager.getVal>(PrefName.AnimeExtensionRepos) + val animeRepos = + PrefManager.getVal>(PrefName.AnimeExtensionRepos) AddRepositoryBottomSheet.newInstance( MediaType.ANIME, animeRepos.toList(), onRepositoryAdded = { input, mediaType -> - processUserInput(input, mediaType, it.attachView) + AddRepositoryBottomSheet.addRepo(input, mediaType) + setExtensionOutput(it.attachView, mediaType) }, - onRepositoryRemoved = { item -> - val repos = PrefManager.getVal>(PrefName.AnimeExtensionRepos).minus(item) - PrefManager.setVal(PrefName.AnimeExtensionRepos, repos) - setExtensionOutput(it.attachView, MediaType.ANIME) + onRepositoryRemoved = { item, mediaType -> + AddRepositoryBottomSheet.removeRepo(item, mediaType) + setExtensionOutput(it.attachView, mediaType) } ).show(supportFragmentManager, "add_repo") }, @@ -172,17 +115,18 @@ class SettingsExtensionsActivity : AppCompatActivity() { desc = getString(R.string.manga_add_repository_desc), icon = R.drawable.ic_github, onClick = { - val mangaRepos = PrefManager.getVal>(PrefName.MangaExtensionRepos) + val mangaRepos = + PrefManager.getVal>(PrefName.MangaExtensionRepos) AddRepositoryBottomSheet.newInstance( MediaType.MANGA, mangaRepos.toList(), onRepositoryAdded = { input, mediaType -> - processUserInput(input, mediaType, it.attachView) + AddRepositoryBottomSheet.addRepo(input, mediaType) + setExtensionOutput(it.attachView, mediaType) }, - onRepositoryRemoved = { item -> - val repos = PrefManager.getVal>(PrefName.MangaExtensionRepos).minus(item) - PrefManager.setVal(PrefName.MangaExtensionRepos, repos) - setExtensionOutput(it.attachView, MediaType.MANGA) + onRepositoryRemoved = { item, mediaType -> + AddRepositoryBottomSheet.removeRepo(item, mediaType) + setExtensionOutput(it.attachView, mediaType) } ).show(supportFragmentManager, "add_repo") }, @@ -190,6 +134,31 @@ class SettingsExtensionsActivity : AppCompatActivity() { setExtensionOutput(it.attachView, MediaType.MANGA) } ), + Settings( + type = 1, + name = getString(R.string.novel_add_repository), + desc = getString(R.string.novel_add_repository_desc), + icon = R.drawable.ic_github, + onClick = { + val novelRepos = + PrefManager.getVal>(PrefName.NovelExtensionRepos) + AddRepositoryBottomSheet.newInstance( + MediaType.NOVEL, + novelRepos.toList(), + onRepositoryAdded = { input, mediaType -> + AddRepositoryBottomSheet.addRepo(input, mediaType) + setExtensionOutput(it.attachView, mediaType) + }, + onRepositoryRemoved = { item, mediaType -> + AddRepositoryBottomSheet.removeRepo(item, mediaType) + setExtensionOutput(it.attachView, mediaType) + } + ).show(supportFragmentManager, "add_repo") + }, + attach = { + setExtensionOutput(it.attachView, MediaType.NOVEL) + } + ), Settings( type = 1, name = getString(R.string.extension_test), @@ -217,7 +186,10 @@ class SettingsExtensionsActivity : AppCompatActivity() { setTitle(R.string.user_agent) setCustomView(dialogView.root) setPosButton(R.string.ok) { - PrefManager.setVal(PrefName.DefaultUserAgent, editText.text.toString()) + PrefManager.setVal( + PrefName.DefaultUserAgent, + editText.text.toString() + ) } setNeutralButton(R.string.reset) { PrefManager.removeVal(PrefName.DefaultUserAgent) @@ -247,7 +219,7 @@ class SettingsExtensionsActivity : AppCompatActivity() { ProxyDialogFragment().show(supportFragmentManager, "dialog") } ), - Settings( + Settings( type = 2, name = getString(R.string.force_legacy_installer), desc = getString(R.string.force_legacy_installer_desc), diff --git a/app/src/main/java/ani/dantotsu/settings/saving/PrefManager.kt b/app/src/main/java/ani/dantotsu/settings/saving/PrefManager.kt index c0833ee2..c7e9b880 100644 --- a/app/src/main/java/ani/dantotsu/settings/saving/PrefManager.kt +++ b/app/src/main/java/ani/dantotsu/settings/saving/PrefManager.kt @@ -255,9 +255,6 @@ object PrefManager { return allEntries } - - - @Suppress("UNCHECKED_CAST") fun getLiveVal(prefName: PrefName, default: T): SharedPreferenceLiveData { val pref = getPrefLocation(prefName.data.prefLocation) @@ -298,7 +295,11 @@ object PrefManager { default as Set ) as SharedPreferenceLiveData - else -> throw IllegalArgumentException("Type not supported") + else -> SharedPreferenceClassLiveData( + pref, + prefName.name, + default + ) } } @@ -326,6 +327,11 @@ object PrefManager { this as? SharedPreferenceStringSetLiveData ?: throw ClassCastException("Cannot cast to SharedPreferenceLiveData>") + @Suppress("UNCHECKED_CAST") + inline fun SharedPreferenceLiveData<*>.asLiveClass(): SharedPreferenceClassLiveData = + this as? SharedPreferenceClassLiveData + ?: throw ClassCastException("Cannot cast to SharedPreferenceLiveData") + fun getAnimeDownloadPreferences(): SharedPreferences = animeDownloadsPreferences!! //needs to be used externally diff --git a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt index 659e399b..2aaa25ee 100644 --- a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt +++ b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt @@ -3,12 +3,13 @@ package ani.dantotsu.settings.saving import android.graphics.Color import ani.dantotsu.connections.comments.AuthResponse import ani.dantotsu.connections.mal.MAL +import ani.dantotsu.media.SearchHistory import ani.dantotsu.notifications.comment.CommentStore import ani.dantotsu.notifications.subscription.SubscriptionStore import ani.dantotsu.settings.saving.internal.Location import ani.dantotsu.settings.saving.internal.Pref -enum class PrefName(val data: Pref) { //TODO: Split this into multiple files +enum class PrefName(val data: Pref) { //General SharedUserID(Pref(Location.General, Boolean::class, true)), OfflineView(Pref(Location.General, Int::class, 0)), @@ -32,10 +33,15 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files ), AnimeExtensionRepos(Pref(Location.General, Set::class, setOf())), MangaExtensionRepos(Pref(Location.General, Set::class, setOf())), + NovelExtensionRepos(Pref(Location.General, Set::class, setOf())), AnimeSourcesOrder(Pref(Location.General, List::class, listOf())), - AnimeSearchHistory(Pref(Location.General, Set::class, setOf())), MangaSourcesOrder(Pref(Location.General, List::class, listOf())), - MangaSearchHistory(Pref(Location.General, Set::class, setOf())), + SortedAnimeSH(Pref(Location.General, List::class, listOf())), + SortedMangaSH(Pref(Location.General, List::class, listOf())), + SortedCharacterSH(Pref(Location.General, List::class, listOf())), + SortedStaffSH(Pref(Location.General, List::class, listOf())), + SortedStudioSH(Pref(Location.General, List::class, listOf())), + SortedUserSH(Pref(Location.General, List::class, listOf())), NovelSourcesOrder(Pref(Location.General, List::class, listOf())), CommentNotificationInterval(Pref(Location.General, Int::class, 0)), AnilistNotificationInterval(Pref(Location.General, Int::class, 3)), @@ -49,6 +55,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files CommentsEnabled(Pref(Location.General, Int::class, 0)), EnableSocks5Proxy(Pref(Location.General, Boolean::class, false)), ProxyAuthEnabled(Pref(Location.General, Boolean::class, false)), + AniMangaSearchDirect(Pref(Location.General, Boolean::class, true)), //User Interface UseOLED(Pref(Location.UI, Boolean::class, false)), diff --git a/app/src/main/java/ani/dantotsu/settings/saving/SharedPreferenceLiveData.kt b/app/src/main/java/ani/dantotsu/settings/saving/SharedPreferenceLiveData.kt index 562abbc6..83eedef2 100644 --- a/app/src/main/java/ani/dantotsu/settings/saving/SharedPreferenceLiveData.kt +++ b/app/src/main/java/ani/dantotsu/settings/saving/SharedPreferenceLiveData.kt @@ -1,7 +1,11 @@ package ani.dantotsu.settings.saving import android.content.SharedPreferences +import android.util.Base64 import androidx.lifecycle.LiveData +import ani.dantotsu.util.Logger +import java.io.ByteArrayInputStream +import java.io.ObjectInputStream abstract class SharedPreferenceLiveData( val sharedPrefs: SharedPreferences, @@ -78,6 +82,41 @@ class SharedPreferenceStringSetLiveData( sharedPrefs.getStringSet(key, defValue)?.toSet() ?: defValue } +@Suppress("UNCHECKED_CAST") +class SharedPreferenceClassLiveData( + sharedPrefs: SharedPreferences, + key: String, + defValue: T +) : SharedPreferenceLiveData(sharedPrefs, key, defValue) { + override fun getValueFromPreferences(key: String, defValue: T): T { + return try { + val serialized = sharedPrefs.getString(key, null) + if (serialized != null) { + val data = Base64.decode(serialized, Base64.DEFAULT) + val bis = ByteArrayInputStream(data) + val ois = ObjectInputStream(bis) + val obj = ois.readObject() as T + obj + } else { + Logger.log("Serialized data is null (key: $key)") + defValue + } + } catch (e: java.io.InvalidClassException) { + Logger.log(e) + try { + sharedPrefs.edit().remove(key).apply() + defValue + } catch (e: Exception) { + Logger.log(e) + defValue + } + } catch (e: Exception) { + Logger.log(e) + defValue + } + } +} + @Suppress("unused") fun SharedPreferences.intLiveData(key: String, defValue: Int): SharedPreferenceLiveData { return SharedPreferenceIntLiveData(this, key, defValue) diff --git a/app/src/main/java/ani/dantotsu/util/StoragePermissions.kt b/app/src/main/java/ani/dantotsu/util/StoragePermissions.kt index ee2a9aa8..34446f96 100644 --- a/app/src/main/java/ani/dantotsu/util/StoragePermissions.kt +++ b/app/src/main/java/ani/dantotsu/util/StoragePermissions.kt @@ -8,7 +8,6 @@ import android.net.Uri import android.os.Build import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat diff --git a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt index a22ee27b..e1017bec 100644 --- a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt @@ -29,8 +29,6 @@ class SourcePreferences( fun migrationSortingDirection() = preferenceStore.getEnum("pref_migration_direction", SetMigrateSorting.Direction.ASCENDING) - fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet()) - // Mixture Sources fun disabledAnimeSources() = preferenceStore.getStringSet("hidden_anime_catalogues", emptySet()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/AnimeExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/AnimeExtensionManager.kt index 07809c60..04303aba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/AnimeExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/AnimeExtensionManager.kt @@ -253,45 +253,6 @@ class AnimeExtensionManager( installer.uninstallApk(pkgName) } - /** - * Adds the given signature to the list of trusted signatures. It also loads in background the - * anime extensions that match this signature. - * - * @param signature The signature to whitelist. - */ - @OptIn(DelicateCoroutinesApi::class) - fun trustSignature(signature: String) { - val untrustedSignatures = - _untrustedAnimeExtensionsFlow.value.map { it.signatureHash }.toSet() - if (signature !in untrustedSignatures) return - - ExtensionLoader.trustedSignaturesAnime += signature - preferences.trustedSignatures() += signature - - val nowTrustedAnimeExtensions = - _untrustedAnimeExtensionsFlow.value.filter { it.signatureHash == signature } - _untrustedAnimeExtensionsFlow.value -= nowTrustedAnimeExtensions - - val ctx = context - launchNow { - nowTrustedAnimeExtensions - .map { animeextension -> - async { - ExtensionLoader.loadAnimeExtensionFromPkgName( - ctx, - animeextension.pkgName - ) - } - } - .map { it.await() } - .forEach { result -> - if (result is AnimeLoadResult.Success) { - registerNewExtension(result.extension) - } - } - } - } - /** * Registers the given anime extension in this and the source managers. * @@ -375,7 +336,7 @@ class AnimeExtensionManager( private fun AnimeExtension.Installed.updateExists(availableAnimeExtension: AnimeExtension.Available? = null): Boolean { val availableExt = availableAnimeExtension ?: _availableAnimeExtensionsFlow.value.find { it.pkgName == pkgName } - if (isUnofficial || availableExt == null) return false + if (availableExt == null) return false return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt index d0a7af33..a57f1f7d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt @@ -1,5 +1,7 @@ package eu.kanade.tachiyomi.extension.api +import ani.dantotsu.parsers.novel.AvailableNovelSources +import ani.dantotsu.parsers.novel.NovelExtension import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.util.Logger @@ -66,13 +68,18 @@ internal class ExtensionGithubApi { PrefManager.getVal>(PrefName.AnimeExtensionRepos).toMutableList() repos.forEach { + val repoUrl = if (it.contains("index.min.json")) { + it + } else { + "$it${if (it.endsWith('/')) "" else "/"}index.min.json" + } try { val githubResponse = try { networkService.client - .newCall(GET("${it}/index.min.json")) + .newCall(GET(repoUrl)) .awaitSuccess() } catch (e: Throwable) { - Logger.log("Failed to get repo: $it") + Logger.log("Failed to get repo: $repoUrl") Logger.log(e) null } @@ -101,7 +108,7 @@ internal class ExtensionGithubApi { } fun getAnimeApkUrl(extension: AnimeExtension.Available): String { - return "${extension.repository}/apk/${extension.apkName}" + return "${extension.repository.removeSuffix("index.min.json")}/apk/${extension.apkName}" } private fun List.toMangaExtensionSources(): List { @@ -189,7 +196,93 @@ internal class ExtensionGithubApi { } fun getMangaApkUrl(extension: MangaExtension.Available): String { - return "${extension.repository}/apk/${extension.apkName}" + return "${extension.repository.removeSuffix("index.min.json")}/apk/${extension.apkName}" + } + + suspend fun findNovelExtensions(): List { + return withIOContext { + + val extensions: ArrayList = arrayListOf() + + val repos = + PrefManager.getVal>(PrefName.NovelExtensionRepos).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(repoUrl)) + .awaitSuccess() + } catch (e: Throwable) { + Logger.log("Failed to get repo: $repoUrl") + Logger.log(e) + null + } + + val response = githubResponse ?: run { + networkService.client + .newCall(GET(fallbackRepoUrl(it) + "/index.min.json")) + .awaitSuccess() + } + + val repoExtensions = with(json) { + response + .parseAs>() + .toNovelExtensions(it) + } + + extensions.addAll(repoExtensions) + } catch (e: Throwable) { + Logger.log("Failed to get extensions from GitHub") + Logger.log(e) + } + } + + extensions + } + } + + private fun List.toNovelExtensions(repository: String): List { + return mapNotNull { extension -> + val sources = extension.sources?.map { source -> + ExtensionSourceJsonObject( + source.id, + source.lang, + source.name, + source.baseUrl, + ) + } + val iconUrl = "${repository.removeSuffix("/index.min.json")}/icon/${extension.pkg}.png" + NovelExtension.Available( + extension.name, + extension.pkg, + extension.apk, + extension.code, + repository, + sources?.toNovelSources() ?: emptyList(), + iconUrl, + ) + } + } + + private fun List.toNovelSources(): List { + return map { source -> + AvailableNovelSources( + source.id, + source.lang, + source.name, + source.baseUrl, + ) + } + } + + fun getNovelApkUrl(extension: NovelExtension.Available): String { + return "${extension.repository.removeSuffix("index.min.json")}/apk/${extension.pkgName}.apk" } private fun fallbackRepoUrl(repoUrl: String): String? { diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/MangaExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/MangaExtensionManager.kt index 1ae39724..f18667ec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/MangaExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/MangaExtensionManager.kt @@ -249,44 +249,6 @@ class MangaExtensionManager( installer.uninstallApk(pkgName) } - /** - * Adds the given signature to the list of trusted signatures. It also loads in background the - * extensions that match this signature. - * - * @param signature The signature to whitelist. - */ - @OptIn(DelicateCoroutinesApi::class) - fun trustSignature(signature: String) { - val untrustedSignatures = _untrustedExtensionsFlow.value.map { it.signatureHash }.toSet() - if (signature !in untrustedSignatures) return - - ExtensionLoader.trustedSignaturesManga += signature - preferences.trustedSignatures() += signature - - val nowTrustedExtensions = - _untrustedExtensionsFlow.value.filter { it.signatureHash == signature } - _untrustedExtensionsFlow.value -= nowTrustedExtensions - - val ctx = context - launchNow { - nowTrustedExtensions - .map { extension -> - async { - ExtensionLoader.loadMangaExtensionFromPkgName( - ctx, - extension.pkgName - ) - } - } - .map { it.await() } - .forEach { result -> - if (result is MangaLoadResult.Success) { - registerNewExtension(result.extension) - } - } - } - } - /** * Registers the given extension in this and the source managers. * @@ -368,7 +330,7 @@ class MangaExtensionManager( private fun MangaExtension.Installed.updateExists(availableExtension: MangaExtension.Available? = null): Boolean { val availableExt = availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName } - if (isUnofficial || availableExt == null) return false + if (availableExt == null) return false return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index fdaa245a..e8c41d15 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -73,27 +73,6 @@ internal object ExtensionLoader { (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) PackageManager.GET_SIGNING_CERTIFICATES else 0) - // jmir1's key - private const val officialSignatureAnime = - "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c" - - var trustedSignaturesAnime = - mutableSetOf() + preferences.trustedSignatures().get() + officialSignatureAnime - - // inorichi's key - private const val officialSignatureManga = - "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" - - //dan's key - private const val officialSignature = - "a3061edb369278749b8e8de810d440d38e96417bbd67bbdfc5d9d9ed475ce4a5" - - /** - * List of the trusted signatures. - */ - var trustedSignaturesManga = - mutableSetOf() + preferences.trustedSignatures().get() + officialSignatureManga - /** * Return a list of all the installed extensions initialized concurrently. * @@ -256,8 +235,6 @@ internal object ExtensionLoader { return AnimeLoadResult.Error } - val signatureHash = getSignatureHash(pkgInfo) - val isNsfw = appInfo.metaData.getInt("$ANIME_PACKAGE$XX_METADATA_NSFW") == 1 if (!loadNsfwSource && isNsfw) { Logger.log("NSFW extension $pkgName not allowed") @@ -321,7 +298,7 @@ internal object ExtensionLoader { hasChangelog = hasChangelog, sources = sources, pkgFactory = appInfo.metaData.getString("$ANIME_PACKAGE$XX_METADATA_SOURCE_FACTORY"), - isUnofficial = signatureHash != officialSignatureAnime, + isUnofficial = true, icon = context.getApplicationIcon(pkgName), ) return AnimeLoadResult.Success(extension) @@ -362,8 +339,6 @@ internal object ExtensionLoader { return MangaLoadResult.Error } - val signatureHash = getSignatureHash(pkgInfo) - val isNsfw = appInfo.metaData.getInt("$MANGA_PACKAGE$XX_METADATA_NSFW") == 1 if (!loadNsfwSource && isNsfw) { Logger.log("NSFW extension $pkgName not allowed") @@ -427,7 +402,7 @@ internal object ExtensionLoader { hasChangelog = hasChangelog, sources = sources, pkgFactory = appInfo.metaData.getString("$MANGA_PACKAGE$XX_METADATA_SOURCE_FACTORY"), - isUnofficial = signatureHash != officialSignatureManga, + isUnofficial = true, icon = context.getApplicationIcon(pkgName), ) return MangaLoadResult.Success(extension) @@ -458,8 +433,6 @@ internal object ExtensionLoader { return NovelLoadResult.Error(Exception("Missing versionName for extension $extName")) } - val signatureHash = getSignatureHash(pkgInfo) - val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader) val novelInterfaceInstance = try { val className = appInfo.loadLabel(context.packageManager).toString() @@ -479,7 +452,7 @@ internal object ExtensionLoader { versionName = versionName, versionCode = versionCode, sources = listOfNotNull(novelInterfaceInstance), - isUnofficial = signatureHash != officialSignatureManga, + isUnofficial = true, icon = context.getApplicationIcon(pkgName), ) return NovelLoadResult.Success(extension) @@ -505,21 +478,4 @@ internal object ExtensionLoader { } } } - - /** - * Returns the signature hash of the package or null if it's not signed. - * - * @param pkgInfo The package info of the application. - */ - private fun getSignatureHash(pkgInfo: PackageInfo): String? { - val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) - pkgInfo.signingInfo?.signingCertificateHistory - else - @Suppress("DEPRECATION") pkgInfo.signatures - return if (signatures != null && signatures.isNotEmpty()) { - Hash.sha256(signatures.first().toByteArray()) - } else { - null - } - } } diff --git a/app/src/main/res/layout/activity_author.xml b/app/src/main/res/layout/activity_author.xml deleted file mode 100644 index 61a5eb3b..00000000 --- a/app/src/main/res/layout/activity_author.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_character.xml b/app/src/main/res/layout/activity_character.xml index ccc2af4d..55f90601 100644 --- a/app/src/main/res/layout/activity_character.xml +++ b/app/src/main/res/layout/activity_character.xml @@ -104,20 +104,75 @@ android:indeterminate="true" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> - + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + + + + + + + + + + + + + + + + +