Merge pull request #558 from rebelonion/dev

Dev
This commit is contained in:
rebel onion 2025-01-05 08:56:36 -06:00 committed by GitHub
commit 9dd59bb592
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
70 changed files with 2750 additions and 1408 deletions

View file

@ -141,11 +141,12 @@ jobs:
# 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

View file

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

View file

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

View file

@ -394,11 +394,10 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Support both schemes -->
<data android:host="add-repo"/>
<data android:scheme="tachiyomi"/>
<data android:host="add-repo"/>
<data android:scheme="aniyomi"/>
<data android:host="add-repo"/>
<data android:scheme="novelyomi"/>
</intent-filter>
</activity>
<activity

View file

@ -449,19 +449,20 @@ class MainActivity : AppCompatActivity() {
if (uri == null) {
throw Exception("Uri is null")
}
if ((uri.scheme == "tachiyomi" || uri.scheme == "aniyomi") && uri.host == "add-repo") {
if ((uri.scheme == "tachiyomi" || uri.scheme == "aniyomi" || uri.scheme == "novelyomi") && uri.host == "add-repo") {
val url = uri.getQueryParameter("url") ?: throw Exception("No url for repo import")
val prefName = if (uri.scheme == "tachiyomi") {
PrefName.MangaExtensionRepos
} else {
PrefName.AnimeExtensionRepos
val (prefName, name) = when (uri.scheme) {
"tachiyomi" -> PrefName.MangaExtensionRepos to "Manga"
"aniyomi" -> PrefName.AnimeExtensionRepos to "Anime"
"novelyomi" -> PrefName.NovelExtensionRepos to "Novel"
else -> throw Exception("Invalid scheme")
}
val savedRepos: Set<String> = 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")

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,429 @@
package ani.dantotsu.connections.anilist
val standardPageInformation = """
pageInfo {
total
perPage
currentPage
lastPage
hasNextPage
}
""".prepare()
fun String.prepare() = this.trimIndent().replace("\n", " ").replace(""" """, "")
fun characterInformation(includeMediaInfo: Boolean) = """
id
name {
first
middle
last
full
native
userPreferred
}
image {
large
medium
}
age
gender
description
dateOfBirth {
year
month
day
}
${if (includeMediaInfo) """
media(page: 0,sort:[POPULARITY_DESC,SCORE_DESC]) {
$standardPageInformation
edges {
id
voiceActors {
id,
name {
userPreferred
}
languageV2,
image {
medium,
large
}
}
characterRole
node {
id
idMal
isAdult
status
chapters
episodes
nextAiringEpisode { episode }
type
meanScore
isFavourite
format
bannerImage
countryOfOrigin
coverImage { large }
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
}
}
}""".prepare() else ""}
""".prepare()
fun studioInformation(page: Int, perPage: Int) = """
id
name
isFavourite
favourites
media(page: $page, sort:START_DATE_DESC, perPage: $perPage) {
$standardPageInformation
edges {
id
node {
id
idMal
isAdult
status
chapters
episodes
nextAiringEpisode { episode }
type
meanScore
startDate{ year }
isFavourite
format
bannerImage
countryOfOrigin
coverImage { large }
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
}
}
}
""".prepare()
fun staffInformation(page: Int, perPage: Int) = """
id
name {
first
middle
last
full
native
userPreferred
}
image {
large
medium
}
dateOfBirth {
year
month
day
}
dateOfDeath {
year
month
day
}
age
yearsActive
homeTown
staffMedia(page: $page,sort:START_DATE_DESC, perPage: $perPage) {
$standardPageInformation
edges {
staffRole
id
node {
id
idMal
isAdult
status
chapters
episodes
nextAiringEpisode { episode }
type
meanScore
startDate{ year }
isFavourite
format
bannerImage
countryOfOrigin
coverImage { large }
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
}
}
}
""".prepare()
fun userInformation() = """
id
name
about(asHtml: true)
avatar {
large
medium
}
bannerImage
isFollowing
isFollower
isBlocked
siteUrl
""".prepare()
fun aniMangaSearch(perPage: Int?) = """
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) {
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
$standardPageInformation
media(id: ${"$"}id, type: ${"$"}type, season: ${"$"}season, format_in: ${"$"}format, status: ${"$"}status, countryOfOrigin: ${"$"}countryOfOrigin, source: ${"$"}source, search: ${"$"}search, onList: ${"$"}onList, seasonYear: ${"$"}seasonYear, startDate_like: ${"$"}year, startDate_lesser: ${"$"}yearLesser, startDate_greater: ${"$"}yearGreater, episodes_lesser: ${"$"}episodeLesser, episodes_greater: ${"$"}episodeGreater, duration_lesser: ${"$"}durationLesser, duration_greater: ${"$"}durationGreater, chapters_lesser: ${"$"}chapterLesser, chapters_greater: ${"$"}chapterGreater, volumes_lesser: ${"$"}volumeLesser, volumes_greater: ${"$"}volumeGreater, licensedBy_in: ${"$"}licensedBy, isLicensed: ${"$"}isLicensed, genre_in: ${"$"}genres, genre_not_in: ${"$"}excludedGenres, tag_in: ${"$"}tags, tag_not_in: ${"$"}excludedTags, minimumTagRank: ${"$"}minimumTagRank, sort: ${"$"}sort, isAdult: ${"$"}isAdult) {
${standardMediaInformation()}
}
}
}
""".prepare()
fun standardMediaInformation() = """
id
idMal
siteUrl
isAdult
status(version: 2)
chapters
episodes
nextAiringEpisode {
episode
airingAt
}
type
genres
meanScore
popularity
favourites
isFavourite
format
bannerImage
countryOfOrigin
coverImage {
large
extraLarge
}
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
""".prepare()
fun fullMediaInformation(id: Int) = """
{
Media(id: $id) {
streamingEpisodes {
title
thumbnail
url
site
}
mediaListEntry {
id
status
score(format: POINT_100)
progress
private
notes
repeat
customLists
updatedAt
startedAt {
year
month
day
}
completedAt {
year
month
day
}
}
reviews(perPage: 3, sort: SCORE_DESC) {
nodes {
id
mediaId
mediaType
summary
body(asHtml: true)
rating
ratingAmount
userRating
score
private
siteUrl
createdAt
updatedAt
user {
id
name
bannerImage
avatar {
medium
large
}
}
}
}
${standardMediaInformation()}
source
duration
season
seasonYear
startDate {
year
month
day
}
endDate {
year
month
day
}
studios(isMain: true) {
nodes {
id
name
siteUrl
}
}
description
trailer {
site
id
}
synonyms
tags {
name
rank
isMediaSpoiler
}
characters(sort: [ROLE, FAVOURITES_DESC], perPage: 25, page: 1) {
edges {
role
voiceActors {
id
name {
first
middle
last
full
native
userPreferred
}
image {
large
medium
}
languageV2
}
node {
id
image {
medium
}
name {
userPreferred
}
isFavourite
}
}
}
relations {
edges {
relationType(version: 2)
node {
${standardMediaInformation()}
}
}
}
staffPreview: staff(perPage: 8, sort: [RELEVANCE, ID]) {
edges {
role
node {
id
image {
large
medium
}
name {
userPreferred
}
}
}
}
recommendations(sort: RATING_DESC) {
nodes {
mediaRecommendation {
${standardMediaInformation()}
}
}
}
externalLinks {
url
site
}
}
Page(page: 1) {
$standardPageInformation
mediaList(isFollowing: true, sort: [STATUS], mediaId: $id) {
id
status
score(format: POINT_100)
progress
progressVolumes
user {
id
name
avatar {
large
medium
}
}
}
}
}
""".prepare()

View file

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

View file

@ -260,7 +260,7 @@ class DownloadCompat {
"$mangaLink/${it.name}",
it.name,
null,
null,
"Unknown",
SChapter.create()
)
chapters.add(chapter)

View file

@ -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}"
}

View file

@ -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<CrashlyticsInterface>().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<ImageData>,
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

View file

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

View file

@ -13,7 +13,6 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -21,7 +20,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<AnimePageAdapter.AnimePageViewHold
updateAvatar()
trendingBinding.searchBar.hint = "ANIME"
trendingBinding.searchBar.hint = binding.root.context.getString(R.string.search)
trendingBinding.searchBarText.setOnClickListener {
val context = binding.root.context
if (PrefManager.getVal(PrefName.AniMangaSearchDirect) && Anilist.token != null) {
ContextCompat.startActivity(
it.context,
Intent(it.context, SearchActivity::class.java).putExtra("type", "ANIME"),
context,
Intent(context, SearchActivity::class.java).putExtra("type", "ANIME"),
null
)
} else {
SearchBottomSheet.newInstance().show(
(context as AppCompatActivity).supportFragmentManager,
"search"
)
}
}
trendingBinding.userAvatar.setSafeOnClickListener {

View file

@ -48,7 +48,6 @@ import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.util.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
@ -134,6 +133,12 @@ class HomeFragment : Fragment() {
"dialog"
)
}
binding.searchImageContainer.setSafeOnClickListener {
SearchBottomSheet.newInstance().show(
(it.context as androidx.appcompat.app.AppCompatActivity).supportFragmentManager,
"search"
)
}
binding.homeUserAvatarContainer.setOnLongClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
ContextCompat.startActivity(

View file

@ -1,12 +1,10 @@
package ani.dantotsu.home
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
@ -20,7 +18,6 @@ import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.textfield.TextInputEditText
class LoginFragment : Fragment() {

View file

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

View file

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

View file

@ -0,0 +1,74 @@
package ani.dantotsu.home
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType.Companion.toAnilistString
import ani.dantotsu.databinding.BottomSheetSearchBinding
import ani.dantotsu.media.SearchActivity
class SearchBottomSheet : BottomSheetDialogFragment() {
private var _binding: BottomSheetSearchBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetSearchBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.animeSearch.setOnClickListener {
startActivity(requireContext(), SearchType.ANIME)
dismiss()
}
binding.mangaSearch.setOnClickListener {
startActivity(requireContext(), SearchType.MANGA)
dismiss()
}
binding.characterSearch.setOnClickListener {
startActivity(requireContext(), SearchType.CHARACTER)
dismiss()
}
binding.staffSearch.setOnClickListener {
startActivity(requireContext(), SearchType.STAFF)
dismiss()
}
binding.studioSearch.setOnClickListener {
startActivity(requireContext(), SearchType.STUDIO)
dismiss()
}
binding.userSearch.setOnClickListener {
startActivity(requireContext(), SearchType.USER)
dismiss()
}
}
private fun startActivity(context: Context, type: SearchType) {
ContextCompat.startActivity(
context,
Intent(context, SearchActivity::class.java).putExtra("type", type.toAnilistString()),
null
)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
fun newInstance() = SearchBottomSheet()
}
}

View file

@ -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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,77 @@
package ani.dantotsu.media
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.databinding.ItemSearchHeaderBinding
abstract class HeaderInterface: RecyclerView.Adapter<HeaderInterface.SearchHeaderViewHolder>() {
private val itemViewType = 6969
var search: Runnable? = null
var requestFocus: Runnable? = null
protected var textWatcher: TextWatcher? = null
protected lateinit var searchHistoryAdapter: SearchHistoryAdapter
protected lateinit var binding: ItemSearchHeaderBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding =
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchHeaderViewHolder(binding)
}
fun setHistoryVisibility(visible: Boolean) {
if (visible) {
binding.searchResultLayout.startAnimation(fadeOutAnimation())
binding.searchHistoryList.startAnimation(fadeInAnimation())
binding.searchResultLayout.visibility = View.GONE
binding.searchHistoryList.visibility = View.VISIBLE
binding.searchByImage.visibility = View.VISIBLE
} else {
if (binding.searchResultLayout.visibility != View.VISIBLE) {
binding.searchResultLayout.startAnimation(fadeInAnimation())
binding.searchHistoryList.startAnimation(fadeOutAnimation())
}
binding.searchResultLayout.visibility = View.VISIBLE
binding.clearHistory.visibility = View.GONE
binding.searchHistoryList.visibility = View.GONE
binding.searchByImage.visibility = View.GONE
}
}
private fun fadeInAnimation(): Animation {
return AlphaAnimation(0f, 1f).apply {
duration = 150
}
}
protected fun fadeOutAnimation(): Animation {
return AlphaAnimation(1f, 0f).apply {
duration = 150
}
}
protected fun updateClearHistoryVisibility() {
binding.clearHistory.visibility =
if (searchHistoryAdapter.itemCount > 0) View.VISIBLE else View.GONE
}
fun addHistory() {
if (::searchHistoryAdapter.isInitialized && binding.searchBarText.text.toString()
.isNotBlank()
) searchHistoryAdapter.add(binding.searchBarText.text.toString())
}
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) :
RecyclerView.ViewHolder(binding.root)
override fun getItemCount(): Int = 1
override fun getItemViewType(position: Int): Int {
return itemViewType
}
}

View file

@ -15,10 +15,16 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistSearch
import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
import ani.dantotsu.connections.anilist.AniMangaSearchResults
import ani.dantotsu.connections.anilist.CharacterSearchResults
import ani.dantotsu.connections.anilist.StaffSearchResults
import ani.dantotsu.connections.anilist.StudioSearchResults
import ani.dantotsu.connections.anilist.UserSearchResults
import ani.dantotsu.databinding.ActivitySearchBinding
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.UsersAdapter
import ani.dantotsu.px
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
@ -35,14 +41,25 @@ class SearchActivity : AppCompatActivity() {
val model: AnilistSearch by viewModels()
var style: Int = 0
lateinit var searchType: SearchType
private var screenWidth: Float = 0f
private lateinit var mediaAdaptor: MediaAdaptor
private lateinit var characterAdaptor: CharacterAdapter
private lateinit var studioAdaptor: StudioAdapter
private lateinit var staffAdaptor: AuthorAdapter
private lateinit var usersAdapter: UsersAdapter
private lateinit var progressAdapter: ProgressAdapter
private lateinit var concatAdapter: ConcatAdapter
private lateinit var headerAdaptor: SearchAdapter
private lateinit var headerAdaptor: HeaderInterface
lateinit var aniMangaResult: AniMangaSearchResults
lateinit var characterResult: CharacterSearchResults
lateinit var studioResult: StudioSearchResults
lateinit var staffResult: StaffSearchResults
lateinit var userResult: UserSearchResults
lateinit var result: SearchResults
lateinit var updateChips: (() -> Unit)
override fun onCreate(savedInstanceState: Bundle?) {
@ -59,16 +76,22 @@ class SearchActivity : AppCompatActivity() {
bottom = navBarHeight + 80f.px
)
val notSet = model.notSet
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
val notSet = model.notSet
if (model.notSet) {
model.notSet = false
model.searchResults = SearchResults(
model.aniMangaSearchResults = AniMangaSearchResults(
intent.getStringExtra("type") ?: "ANIME",
isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false,
isAdult = if (Anilist.adult) intent.getBooleanExtra(
"hentai",
false
) else false,
onList = listOnly,
search = intent.getStringExtra("query"),
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
@ -78,20 +101,92 @@ class SearchActivity : AppCompatActivity() {
source = intent.getStringExtra("source"),
countryOfOrigin = intent.getStringExtra("country"),
season = intent.getStringExtra("season"),
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear")
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra(
"seasonYear"
)
?.toIntOrNull() else null,
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear")
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra(
"seasonYear"
)
?.toIntOrNull() else null,
results = mutableListOf(),
hasNextPage = false
)
}
result = model.searchResults
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)
}
}
}
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,9 +242,11 @@ class SearchActivity : AppCompatActivity() {
}
})
model.getSearch().observe(this) {
when (searchType) {
SearchType.ANIME, SearchType.MANGA -> {
model.getSearch<AniMangaSearchResults>(searchType).observe(this) {
if (it != null) {
model.searchResults.apply {
model.aniMangaSearchResults.apply {
onList = it.onList
isAdult = it.isAdult
perPage = it.perPage
@ -150,13 +267,87 @@ class SearchActivity : AppCompatActivity() {
hasNextPage = it.hasNextPage
}
val prev = model.searchResults.results.size
model.searchResults.results.addAll(it.results)
val prev = model.aniMangaSearchResults.results.size
model.aniMangaSearchResults.results.addAll(it.results)
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
}
SearchType.CHARACTER -> {
model.getSearch<CharacterSearchResults>(searchType).observe(this) {
if (it != null) {
model.characterSearchResults.apply {
search = it.search
page = it.page
hasNextPage = it.hasNextPage
}
val prev = model.characterSearchResults.results.size
model.characterSearchResults.results.addAll(it.results)
characterAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
}
SearchType.STUDIO -> {
model.getSearch<StudioSearchResults>(searchType).observe(this) {
if (it != null) {
model.studioSearchResults.apply {
search = it.search
page = it.page
hasNextPage = it.hasNextPage
}
val prev = model.studioSearchResults.results.size
model.studioSearchResults.results.addAll(it.results)
studioAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
}
SearchType.STAFF -> {
model.getSearch<StaffSearchResults>(searchType).observe(this) {
if (it != null) {
model.staffSearchResults.apply {
search = it.search
page = it.page
hasNextPage = it.hasNextPage
}
val prev = model.staffSearchResults.results.size
model.staffSearchResults.results.addAll(it.results)
staffAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
}
SearchType.USER -> {
model.getSearch<UserSearchResults>(searchType).observe(this) {
if (it != null) {
model.userSearchResults.apply {
search = it.search
page = it.page
hasNextPage = it.hasNextPage
}
val prev = model.userSearchResults.results.size
model.userSearchResults.results.addAll(it.results)
usersAdapter.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
}
}
progressAdapter.ready.observe(this) {
if (it == true) {
@ -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,12 +403,32 @@ 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 {
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
searchTimer.cancel()
@ -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,9 +448,11 @@ class SearchActivity : AppCompatActivity() {
@SuppressLint("NotifyDataSetChanged")
fun recycler() {
if (searchType == SearchType.ANIME || searchType == SearchType.MANGA) {
mediaAdaptor.type = style
mediaAdaptor.notifyDataSetChanged()
}
}
var state: Parcelable? = null
override fun onPause() {

View file

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

View file

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

View file

@ -7,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<String, SearchHistoryAdapter.SearchHistoryViewHolder>(
DIFF_CALLBACK_INSTALLED
) {
private var searchHistoryLiveData: SharedPreferenceStringSetLiveData? = null
private var searchHistory: MutableSet<String>? = null
private var historyType: PrefName = when (type.lowercase(Locale.ROOT)) {
"anime" -> PrefName.AnimeSearchHistory
"manga" -> PrefName.MangaSearchHistory
else -> throw IllegalArgumentException("Invalid type")
private var searchHistoryLiveData: SharedPreferenceClassLiveData<List<SearchHistory>>? = null
private var searchHistory: MutableList<SearchHistory>? = 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<SearchHistory>?.sorted(): List<String>? =
this?.sortedByDescending { it.time }?.map { it.search }
init {
searchHistoryLiveData =
PrefManager.getLiveVal(historyType, mutableSetOf<String>()).asLiveStringSet()
searchHistoryLiveData?.observeForever {
searchHistory = it.toMutableSet()
submitList(searchHistory?.toList())
PrefManager.getLiveVal(historyType, mutableListOf<SearchHistory>()).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(

View file

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

View file

@ -0,0 +1,61 @@
package ani.dantotsu.media
import android.app.Activity
import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ItemCharacterBinding
import ani.dantotsu.loadImage
import ani.dantotsu.setAnimation
import java.io.Serializable
class StudioAdapter(
private val studioList: MutableList<Studio>
) : RecyclerView.Adapter<StudioAdapter.StudioViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StudioViewHolder {
val binding =
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return StudioViewHolder(binding)
}
override fun onBindViewHolder(holder: StudioViewHolder, position: Int) {
val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root)
val studio = studioList.getOrNull(position) ?: return
binding.itemCompactRelation.isVisible = false
binding.itemCompactImage.loadImage(studio.imageUrl)
binding.itemCompactTitle.text = studio.name
}
override fun getItemCount(): Int = studioList.size
inner class StudioViewHolder(val binding: ItemCharacterBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
val studio = studioList[bindingAdapterPosition]
ContextCompat.startActivity(
itemView.context,
Intent(
itemView.context,
StudioActivity::class.java
).putExtra("studio", studio as Serializable),
ActivityOptionsCompat.makeSceneTransitionAnimation(
itemView.context as Activity,
Pair.create(
binding.itemCompactImage,
ViewCompat.getTransitionName(binding.itemCompactImage)!!
),
).toBundle()
)
}
itemView.setOnLongClickListener { copyToClipboard(studioList[bindingAdapterPosition].name ?: ""); true }
}
}
}

View file

@ -0,0 +1,142 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.graphics.drawable.Drawable
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.App.Companion.context
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType.Companion.toAnilistString
import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class SupportingSearchAdapter(private val activity: SearchActivity, private val type: SearchType) :
HeaderInterface() {
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) {
binding = holder.binding
searchHistoryAdapter = SearchHistoryAdapter(type) {
binding.searchBarText.setText(it)
}
binding.searchHistoryList.layoutManager = LinearLayoutManager(binding.root.context)
binding.searchHistoryList.adapter = searchHistoryAdapter
val imm: InputMethodManager =
activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
if (activity.searchType == SearchType.MANGA || activity.searchType == SearchType.ANIME) {
throw IllegalArgumentException("Invalid search type (wrong adapter)")
}
binding.searchByImage.visibility = View.GONE
binding.searchResultGrid.visibility = View.GONE
binding.searchResultList.visibility = View.GONE
binding.searchFilter.visibility = View.GONE
binding.searchAdultCheck.visibility = View.GONE
binding.searchList.visibility = View.GONE
binding.searchChipRecycler.visibility = View.GONE
binding.searchBar.hint = activity.searchType.toAnilistString()
if (PrefManager.getVal(PrefName.Incognito)) {
val startIconDrawableRes = R.drawable.ic_incognito_24
val startIconDrawable: Drawable? =
context?.let { AppCompatResources.getDrawable(it, startIconDrawableRes) }
binding.searchBar.startIconDrawable = startIconDrawable
}
binding.searchBarText.removeTextChangedListener(textWatcher)
when (type) {
SearchType.CHARACTER -> {
binding.searchBarText.setText(activity.characterResult.search)
}
SearchType.STUDIO -> {
binding.searchBarText.setText(activity.studioResult.search)
}
SearchType.STAFF -> {
binding.searchBarText.setText(activity.staffResult.search)
}
SearchType.USER -> {
binding.searchBarText.setText(activity.userResult.search)
}
else -> throw IllegalArgumentException("Invalid search type")
}
binding.clearHistory.setOnClickListener {
it.startAnimation(fadeOutAnimation())
it.visibility = View.GONE
searchHistoryAdapter.clearHistory()
}
updateClearHistoryVisibility()
fun searchTitle() {
val searchText = binding.searchBarText.text.toString().takeIf { it.isNotEmpty() }
val result: SearchResults<*> = when (type) {
SearchType.CHARACTER -> activity.characterResult
SearchType.STUDIO -> activity.studioResult
SearchType.STAFF -> activity.staffResult
SearchType.USER -> activity.userResult
else -> throw IllegalArgumentException("Invalid search type")
}
result.search = searchText
activity.search()
}
textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable) {}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (s.toString().isBlank()) {
activity.emptyMediaAdapter()
CoroutineScope(Dispatchers.IO).launch {
delay(200)
activity.runOnUiThread {
setHistoryVisibility(true)
}
}
} else {
setHistoryVisibility(false)
searchTitle()
}
}
}
binding.searchBarText.addTextChangedListener(textWatcher)
binding.searchBarText.setOnEditorActionListener { _, actionId, _ ->
return@setOnEditorActionListener when (actionId) {
EditorInfo.IME_ACTION_SEARCH -> {
searchTitle()
binding.searchBarText.clearFocus()
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
true
}
else -> false
}
}
binding.searchBar.setEndIconOnClickListener { searchTitle() }
search = Runnable { searchTitle() }
requestFocus = Runnable { binding.searchBarText.requestFocus() }
}
}

View file

@ -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<String, MangaChapter>? = null,
var slug: String? = null,
var author: Author? = null,

View file

@ -40,4 +40,6 @@ data class MangaChapter(
private val dualPages = mutableListOf<Pair<MangaImage, MangaImage?>>()
fun dualPages(): List<Pair<MangaImage, MangaImage?>> = dualPages
fun uniqueNumber(): String = "${number}-${scanlator ?: "Unknown"}"
}

View file

@ -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..<n) {
if (position + i < arr.size) {
val chapterNumber = arr[position + i].number
val chapter = arr[position + i]
val chapterNumber = chapter.uniqueNumber()
if (activeDownloads.contains(chapterNumber)) {
//do nothing
continue
@ -135,8 +136,8 @@ class MangaChapterAdapter(
//do nothing
continue
} else {
fragment.onMangaChapterDownloadClick(chapterNumber)
startDownload(chapterNumber)
fragment.onMangaChapterDownloadClick(chapter)
startDownload(chapter.uniqueNumber())
}
}
}
@ -201,28 +202,29 @@ class MangaChapterAdapter(
init {
itemView.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)
fragment.onMangaChapterClick(arr[bindingAdapterPosition])
}
binding.itemDownload.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
val chapterNumber = arr[bindingAdapterPosition].number
val chapter = arr[bindingAdapterPosition]
val chapterNumber = chapter.uniqueNumber()
if (activeDownloads.contains(chapterNumber)) {
fragment.onMangaChapterStopDownloadClick(chapterNumber)
fragment.onMangaChapterStopDownloadClick(chapter)
return@setOnClickListener
} else if (downloadedChapters.contains(chapterNumber)) {
it.context.customAlertDialog().apply {
setTitle("Delete Chapter")
setMessage("Are you sure you want to delete ${chapterNumber}?")
setPosButton(R.string.delete) {
fragment.onMangaChapterRemoveDownloadClick(chapterNumber)
fragment.onMangaChapterRemoveDownloadClick(chapter)
}
setNegButton(R.string.cancel)
show()
}
return@setOnClickListener
} else {
fragment.onMangaChapterDownloadClick(chapterNumber)
startDownload(chapterNumber)
fragment.onMangaChapterDownloadClick(chapter)
startDownload(chapter.uniqueNumber())
}
}
}
@ -277,7 +279,7 @@ class MangaChapterAdapter(
is ChapterListViewHolder -> {
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

View file

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

View file

@ -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,9 +261,13 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
val chapters = loadedChapters[media.selected!!.sourceIndex]
if (chapters != null) {
headerAdapter.options = getScanlators(chapters)
val filteredChapters = chapters.filterNot { (_, chapter) ->
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

View file

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

View file

@ -499,7 +499,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
sChapter.url,
sChapter.name,
null,
sChapter.scanlator,
sChapter.scanlator ?: "Unknown",
sChapter,
sChapter.date_upload
)

View file

@ -1,3 +1,4 @@
package ani.dantotsu.parsers
import android.graphics.drawable.Drawable

View file

@ -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 {

View file

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

View file

@ -31,13 +31,17 @@ class OfflineMangaParser : MangaParser() {
val chapters = mutableListOf<MangaChapter>()
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<MangaImage> {

View file

@ -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<AvailableNovelSources>,
val iconUrl: String,
) : NovelExtension()

View file

@ -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<NovelExtension.Available> {
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<List<NovelExtensionJsonObject>>()
.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<AnimeExtension.Installed>? {
// 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<AnimeLoadResult.Success>()
.map { it.extension }
val extensionsWithUpdate = mutableListOf<AnimeExtension.Installed>()
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<NovelExtensionJsonObject>.toExtensions(): List<NovelExtension.Available> {
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<NovelExtensionSourceJsonObject>.toSources(): List<AvailableNovelSources> {
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<NovelExtensionSourceJsonObject>?,
)
@Serializable
private data class NovelExtensionSourceJsonObject(
val id: Long,
val lang: String,
val name: String,
val baseUrl: String,
)

View file

@ -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<NovelExtension.Available> = 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<InstallStep> {
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)
}

View file

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

View file

@ -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<ItemRepoBinding>() {
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<String> = 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"))
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<Set<String>>(PrefName.AnimeExtensionRepos)
.plus(validLink)
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
CoroutineScope(Dispatchers.IO).launch {
Injekt.get<AnimeExtensionManager>().findAvailableExtensions()
}
}
MediaType.MANGA -> {
val manga =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos)
.plus(validLink)
PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
CoroutineScope(Dispatchers.IO).launch {
Injekt.get<MangaExtensionManager>().findAvailableExtensions()
}
}
MediaType.NOVEL -> {
val novel =
PrefManager.getVal<Set<String>>(PrefName.NovelExtensionRepos)
.plus(validLink)
PrefManager.setVal(PrefName.NovelExtensionRepos, novel)
CoroutineScope(Dispatchers.IO).launch {
Injekt.get<NovelExtensionManager>().findAvailableExtensions()
}
}
}
}
fun removeRepo(input: String, mediaType: MediaType) {
when (mediaType) {
MediaType.ANIME -> {
val anime =
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos)
.minus(input)
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
CoroutineScope(Dispatchers.IO).launch {
Injekt.get<AnimeExtensionManager>().findAvailableExtensions()
}
}
MediaType.MANGA -> {
val manga =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos)
.minus(input)
PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
CoroutineScope(Dispatchers.IO).launch {
Injekt.get<MangaExtensionManager>().findAvailableExtensions()
}
}
MediaType.NOVEL -> {
val novel =
PrefManager.getVal<Set<String>>(PrefName.NovelExtensionRepos)
.minus(input)
PrefManager.setVal(PrefName.NovelExtensionRepos, novel)
CoroutineScope(Dispatchers.IO).launch {
Injekt.get<NovelExtensionManager>().findAvailableExtensions()
}
}
}
}
fun newInstance(
mediaType: MediaType,
repositories: List<String>,
onRepositoryAdded: (String, MediaType) -> Unit,
onRepositoryRemoved: (String) -> Unit
onRepositoryRemoved: (String, MediaType) -> Unit
): AddRepositoryBottomSheet {
return AddRepositoryBottomSheet().apply {
this.mediaType = mediaType

View file

@ -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<Set<String>>(PrefName.AnimeExtensionRepos).plus(entry)
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager.findAvailableExtensions()
}
}
if (mediaType == MediaType.MANGA) {
val manga =
PrefManager.getVal<Set<String>>(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<Set<String>>(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<Set<String>>(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) {
binding.openSettingsButton.setOnClickListener {
val repos: Set<String> = when (type) {
MediaType.ANIME -> {
R.string.anime_add_repository
PrefManager.getVal(PrefName.AnimeExtensionRepos)
}
MediaType.MANGA -> {
R.string.manga_add_repository
PrefManager.getVal(PrefName.MangaExtensionRepos)
}
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)
}
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.NOVEL -> {
PrefManager.getVal(PrefName.NovelExtensionRepos)
}
}
AddRepositoryBottomSheet.newInstance(
type,
repos.toList(),
AddRepositoryBottomSheet::addRepo,
AddRepositoryBottomSheet::removeRepo
).show(supportFragmentManager, "add_repo")
}
}
}

View file

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

View file

@ -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<BasePreferences>().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,41 +62,17 @@ class SettingsExtensionsActivity : AppCompatActivity() {
PrefName.MangaExtensionRepos
}
else -> {
null
MediaType.NOVEL -> {
PrefName.NovelExtensionRepos
}
}
prefName?.let { repoList ->
PrefManager.getVal<Set<String>>(repoList).forEach { item ->
PrefManager.getVal<Set<String>>(prefName).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<Set<String>>(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)
@ -113,32 +81,6 @@ class SettingsExtensionsActivity : AppCompatActivity() {
}
repoInventory.isVisible = repoInventory.childCount > 0
}
}
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<Set<String>>(PrefName.AnimeExtensionRepos).plus(validLink)
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager.findAvailableExtensions()
}
setExtensionOutput(view, MediaType.ANIME)
}
if (mediaType == MediaType.MANGA) {
val manga =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).plus(validLink)
PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
CoroutineScope(Dispatchers.IO).launch {
mangaExtensionManager.findAvailableExtensions()
}
setExtensionOutput(view, MediaType.MANGA)
}
}
settingsRecyclerView.adapter = SettingsAdapter(
arrayListOf(
@ -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<Set<String>>(PrefName.AnimeExtensionRepos)
val animeRepos =
PrefManager.getVal<Set<String>>(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<Set<String>>(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<Set<String>>(PrefName.MangaExtensionRepos)
val mangaRepos =
PrefManager.getVal<Set<String>>(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<Set<String>>(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<Set<String>>(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)

View file

@ -255,9 +255,6 @@ object PrefManager {
return allEntries
}
@Suppress("UNCHECKED_CAST")
fun <T> getLiveVal(prefName: PrefName, default: T): SharedPreferenceLiveData<T> {
val pref = getPrefLocation(prefName.data.prefLocation)
@ -298,7 +295,11 @@ object PrefManager {
default as Set<String>
) as SharedPreferenceLiveData<T>
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<Set<String>>")
@Suppress("UNCHECKED_CAST")
inline fun <reified T> SharedPreferenceLiveData<*>.asLiveClass(): SharedPreferenceClassLiveData<T> =
this as? SharedPreferenceClassLiveData<T>
?: throw ClassCastException("Cannot cast to SharedPreferenceLiveData<T>")
fun getAnimeDownloadPreferences(): SharedPreferences =
animeDownloadsPreferences!! //needs to be used externally

View file

@ -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<String>())),
MangaExtensionRepos(Pref(Location.General, Set::class, setOf<String>())),
NovelExtensionRepos(Pref(Location.General, Set::class, setOf<String>())),
AnimeSourcesOrder(Pref(Location.General, List::class, listOf<String>())),
AnimeSearchHistory(Pref(Location.General, Set::class, setOf<String>())),
MangaSourcesOrder(Pref(Location.General, List::class, listOf<String>())),
MangaSearchHistory(Pref(Location.General, Set::class, setOf<String>())),
SortedAnimeSH(Pref(Location.General, List::class, listOf<SearchHistory>())),
SortedMangaSH(Pref(Location.General, List::class, listOf<SearchHistory>())),
SortedCharacterSH(Pref(Location.General, List::class, listOf<SearchHistory>())),
SortedStaffSH(Pref(Location.General, List::class, listOf<SearchHistory>())),
SortedStudioSH(Pref(Location.General, List::class, listOf<SearchHistory>())),
SortedUserSH(Pref(Location.General, List::class, listOf<SearchHistory>())),
NovelSourcesOrder(Pref(Location.General, List::class, listOf<String>())),
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)),

View file

@ -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<T>(
val sharedPrefs: SharedPreferences,
@ -78,6 +82,41 @@ class SharedPreferenceStringSetLiveData(
sharedPrefs.getStringSet(key, defValue)?.toSet() ?: defValue
}
@Suppress("UNCHECKED_CAST")
class SharedPreferenceClassLiveData<T>(
sharedPrefs: SharedPreferences,
key: String,
defValue: T
) : SharedPreferenceLiveData<T>(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<Int> {
return SharedPreferenceIntLiveData(this, key, defValue)

View file

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

View file

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

View file

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

View file

@ -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<Set<String>>(PrefName.AnimeExtensionRepos).toMutableList()
repos.forEach {
val repoUrl = if (it.contains("index.min.json")) {
it
} else {
"$it${if (it.endsWith('/')) "" else "/"}index.min.json"
}
try {
val githubResponse = try {
networkService.client
.newCall(GET("${it}/index.min.json"))
.newCall(GET(repoUrl))
.awaitSuccess()
} catch (e: Throwable) {
Logger.log("Failed to get repo: $it")
Logger.log("Failed to get repo: $repoUrl")
Logger.log(e)
null
}
@ -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<ExtensionSourceJsonObject>.toMangaExtensionSources(): List<AvailableMangaSources> {
@ -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<NovelExtension.Available> {
return withIOContext {
val extensions: ArrayList<NovelExtension.Available> = arrayListOf()
val repos =
PrefManager.getVal<Set<String>>(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<List<ExtensionJsonObject>>()
.toNovelExtensions(it)
}
extensions.addAll(repoExtensions)
} catch (e: Throwable) {
Logger.log("Failed to get extensions from GitHub")
Logger.log(e)
}
}
extensions
}
}
private fun List<ExtensionJsonObject>.toNovelExtensions(repository: String): List<NovelExtension.Available> {
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<ExtensionSourceJsonObject>.toNovelSources(): List<AvailableNovelSources> {
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? {

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:background="@drawable/bottom_sheet_background"
android:padding="16dp">
<TextView

View file

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

View file

@ -146,6 +146,36 @@
</FrameLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/searchImageContainer"
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_marginTop="4dp"
android:backgroundTint="@color/nav_bg_inv"
android:scaleX="0.96"
android:scaleY="0.96"
app:cardCornerRadius="26dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/searchImage"
android:layout_width="52dp"
android:layout_height="52dp"
android:scaleType="center"
android:scaleX="1.2"
android:scaleY="1.2"
android:tint="@color/bg_white"
app:srcCompat="@drawable/ic_round_search_24"
tools:ignore="ContentDescription,ImageContrastCheck" />
</com.google.android.material.card.MaterialCardView>
</FrameLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">

View file

@ -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" />
<ImageView
android:id="@+id/repoCopyImageView"
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"
app:srcCompat="@drawable/format_link_24"
app:tint="?attr/colorOnBackground"
tools:ignore="ContentDescription" />
</LinearLayout>

View file

@ -395,6 +395,8 @@
<string name="planned_manga">Planned Manga</string>
<string name="image_long_clicking">Open image by Long Clicking</string>
<string name="always_continue_content">Always continue previous items</string>
<string name="open_animanga_directly">Open Anime/Manga search directly</string>
<string name="open_animanga_directly_info">Open Anime/Manga search on their respective pages directly</string>
<string name="search_source_list">Search next available source</string>
<string name="timestamp_proxy_desc">Useful if you are getting Handshake Fails</string>
<string name="timestamp_proxy">Use Proxy for Timestamps</string>
@ -624,6 +626,9 @@
<string name="age">"__Age:__ "</string>
<string name="birthday">\n"__Birthday:__ "</string>
<string name="gender">\n"__Gender:__ "</string>"
<string name="hometown">\n"__Home Town:__ "</string>
<string name="years_active">\n"__Years Active:__ "</string>
<string name="date_of_death">\n"__Date of Death:__ "</string>
<string name="male">Male</string>
<string name="female">Female</string>
@ -896,6 +901,7 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
<string name="anime_add_repository">Add Anime Repo</string>
<string name="manga_add_repository">Add Manga Repo</string>
<string name="novel_add_repository">Add Novel Repo</string>
<string name="edit_repositories">Edit repositories</string>
<string name="rem_repository">Remove repository?</string>
@ -963,6 +969,7 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
<string name="adult_only_content_desc">Show only adult content in the explore page</string>
<string name="anime_add_repository_desc">Add Anime Extensions from various sources</string>
<string name="manga_add_repository_desc">Add Manga Extensions from various sources</string>
<string name="novel_add_repository_desc">Add Novel Extensions from various sources</string>
<string name="user_agent_desc">Change your default user agent</string>
<string name="force_legacy_installer_desc">Use the legacy installer to install extensions (For older android phones)</string>
<string name="skip_loading_extension_icons_desc">Don\'t load icons of extensions on the extension page</string>
@ -1089,11 +1096,12 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
<string name="textview_sub_stroke">Subtitle Stroke</string>
<string name="textview_sub_bottom_margin">Bottom Margin</string>
<string name="add_repository">Add Repository</string>
<string name="add_repository_desc">A repository link should look like this: https://raw.githubusercontent.com/username/repo/branch/index.min.json</string>
<string name="add_repository_desc">A repository link should look like this: https://raw.githubusercontent.com/username/repo/branch/index.min.json\nOr: username/repo/branch</string>
<string name="current_repositories">Current Repositories</string>
<string name="add_repository_warning">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.</string>
<string name="privacy_policy">Privacy Policy</string>
<string name="privacy_policy_desc">Read our privacy policy</string>
<string name="failed_to_load">Failed to load</string>
<string name="studios">Studios</string>
</resources>