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">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index d68cd3a9..7118334a 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -146,6 +146,36 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_repo.xml b/app/src/main/res/layout/item_repo.xml
index 658110b5..6cbf4cb5 100644
--- a/app/src/main/res/layout/item_repo.xml
+++ b/app/src/main/res/layout/item_repo.xml
@@ -25,6 +25,8 @@
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="10dp"
+ android:scaleX="0.8"
+ android:scaleY="0.8"
android:layout_gravity="center_vertical"
android:layout_marginStart="3dp"
android:background="?android:attr/selectableItemBackground"
@@ -32,4 +34,18 @@
app:tint="?attr/colorOnBackground"
tools:ignore="ContentDescription" />
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 504436d0..3b7fb23f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -395,6 +395,8 @@
Planned Manga
Open image by Long Clicking
Always continue previous items
+ Open Anime/Manga search directly
+ Open Anime/Manga search on their respective pages directly
Search next available source
Useful if you are getting Handshake Fails
Use Proxy for Timestamps
@@ -624,6 +626,9 @@
"__Age:__ "
\n"__Birthday:__ "
\n"__Gender:__ ""
+ \n"__Home Town:__ "
+ \n"__Years Active:__ "
+ \n"__Date of Death:__ "
Male
Female
@@ -896,6 +901,7 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
Add Anime Repo
Add Manga Repo
+ Add Novel Repo
Edit repositories
Remove repository?
@@ -963,6 +969,7 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
Show only adult content in the explore page
Add Anime Extensions from various sources
Add Manga Extensions from various sources
+ Add Novel Extensions from various sources
Change your default user agent
Use the legacy installer to install extensions (For older android phones)
Don\'t load icons of extensions on the extension page
@@ -1089,11 +1096,12 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
Subtitle Stroke
Bottom Margin
Add Repository
- A repository link should look like this: https://raw.githubusercontent.com/username/repo/branch/index.min.json
+ A repository link should look like this: https://raw.githubusercontent.com/username/repo/branch/index.min.json\nOr: username/repo/branch
Current Repositories
Warning: Extensions from the repository can run arbitrary code on your device. Only use repositories you trust. \n\nBy adding a repository, you agree to: \n\n1. Not use the app for viewing or distributing copyrighted content. \n2. Not use the app for any illegal activities. \n3. Not use the app for any activities that violate the terms of service of the content providers. \n\nThe app or it\'s maintainer are not affiliated in any way with extension providers. The developers are not responsible for any damages caused by the app. \n\nBy adding a repository, you agree to these terms.
Privacy Policy
Read our privacy policy
Failed to load
+ Studios