diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4c267042..1a6ab597 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -202,7 +202,7 @@ android:parentActivityName=".MainActivity" /> + android:windowSoftInputMode="adjustResize|stateVisible" /> = PrefManager.getVal(PrefName.HomeLayout) if (toShow.getOrNull(7) != true) return null - var query = """{""" - query += "Page1:${status(1)}Page2:${status(2)}" - query += """}""".trimEnd(',') + val query = """{Page1:${status(1)}Page2:${status(2)}}""" val response = executeQuery(query, show = true) val list = mutableListOf() val threeDaysAgo = Calendar.getInstance().apply { @@ -461,8 +460,9 @@ class AnilistQueries { } } - if (anilistActivities.isEmpty() && Anilist.token != null){ - anilistActivities.add(0, + if (anilistActivities.isEmpty() && Anilist.token != null) { + anilistActivities.add( + 0, User( Anilist.userid!!, Anilist.username!!, @@ -477,206 +477,176 @@ class AnilistQueries { } else return null } - suspend fun initHomePage(): Map> { + suspend fun initHomePage(): Map> { val removeList = PrefManager.getCustomVal("removeList", setOf()) val hidePrivate = PrefManager.getVal(PrefName.HidePrivate) val removedMedia = ArrayList() val toShow: List = - PrefManager.getVal(PrefName.HomeLayout) // anime continue, anime fav, anime planned, manga continue, manga fav, manga planned, recommendations - var query = """{""" - if (toShow.getOrNull(0) == true) query += """currentAnime: ${ - continueMediaQuery( - "ANIME", - "CURRENT" - ) - }, repeatingAnime: ${continueMediaQuery("ANIME", "REPEATING")}""" - if (toShow.getOrNull(1) == true) query += """favoriteAnime: ${favMediaQuery(true, 1)}""" - if (toShow.getOrNull(2) == true) query += """plannedAnime: ${ - continueMediaQuery( - "ANIME", - "PLANNING" - ) - }""" - if (toShow.getOrNull(3) == true) query += """currentManga: ${ - continueMediaQuery( - "MANGA", - "CURRENT" - ) - }, repeatingManga: ${continueMediaQuery("MANGA", "REPEATING")}""" - if (toShow.getOrNull(4) == true) query += """favoriteManga: ${favMediaQuery(false, 1)}""" - if (toShow.getOrNull(5) == true) query += """plannedManga: ${ - continueMediaQuery( - "MANGA", - "PLANNING" - ) - }""" - if (toShow.getOrNull(6) == true) query += """recommendationQuery: ${recommendationQuery()}, recommendationPlannedQueryAnime: ${ - recommendationPlannedQuery( - "ANIME" - ) - }, recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}""" - query += """}""".trimEnd(',') + PrefManager.getVal(PrefName.HomeLayout) // list of booleans for what to show + val queries = mutableListOf() + if (toShow.getOrNull(0) == true) { + queries.add("""currentAnime: ${continueMediaQuery("ANIME", "CURRENT")}""") + queries.add("""repeatingAnime: ${continueMediaQuery("ANIME", "REPEATING")}""") + } + if (toShow.getOrNull(1) == true) queries.add("""favoriteAnime: ${favMediaQuery(true, 1)}""") + if (toShow.getOrNull(2) == true) queries.add( + """plannedAnime: ${ + continueMediaQuery( + "ANIME", + "PLANNING" + ) + }""" + ) + if (toShow.getOrNull(3) == true) { + queries.add("""currentManga: ${continueMediaQuery("MANGA", "CURRENT")}""") + queries.add("""repeatingManga: ${continueMediaQuery("MANGA", "REPEATING")}""") + } + if (toShow.getOrNull(4) == true) queries.add( + """favoriteManga: ${ + favMediaQuery( + false, + 1 + ) + }""" + ) + if (toShow.getOrNull(5) == true) queries.add( + """plannedManga: ${ + continueMediaQuery( + "MANGA", + "PLANNING" + ) + }""" + ) + if (toShow.getOrNull(6) == true) { + queries.add("""recommendationQuery: ${recommendationQuery()}""") + queries.add("""recommendationPlannedQueryAnime: ${recommendationPlannedQuery("ANIME")}""") + queries.add("""recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}""") + } + + val query = "{${queries.joinToString(",")}}" val response = executeQuery(query, show = true) - val returnMap = mutableMapOf>() - fun current(type: String) { + val returnMap = mutableMapOf>() + + fun processMedia( + type: String, + currentMedia: List?, + repeatingMedia: List? + ) { val subMap = mutableMapOf() val returnArray = arrayListOf() - val current = - if (type == "Anime") response?.data?.currentAnime else response?.data?.currentManga - val repeating = - if (type == "Anime") response?.data?.repeatingAnime else response?.data?.repeatingManga - current?.lists?.forEach { li -> - li.entries?.reversed()?.forEach { - val m = Media(it) - if (m.id !in removeList && if (hidePrivate) !m.isListPrivate else true) { - m.cameFromContinue = true - subMap[m.id] = m - } else { - removedMedia.add(m) - } + + (currentMedia ?: emptyList()).forEach { entry -> + val media = Media(entry) + if (media.id !in removeList && (!hidePrivate || !media.isListPrivate)) { + media.cameFromContinue = true + subMap[media.id] = media + } else { + removedMedia.add(media) } } - repeating?.lists?.forEach { li -> - li.entries?.reversed()?.forEach { - val m = Media(it) - if (m.id !in removeList && if (hidePrivate) !m.isListPrivate else true) { - m.cameFromContinue = true - subMap[m.id] = m - } else { - removedMedia.add(m) - } + (repeatingMedia ?: emptyList()).forEach { entry -> + val media = Media(entry) + if (media.id !in removeList && (!hidePrivate || !media.isListPrivate)) { + media.cameFromContinue = true + subMap[media.id] = media + } else { + removedMedia.add(media) } } - if (type != "Anime") { + @Suppress("UNCHECKED_CAST") + val list = PrefManager.getNullableCustomVal( + "continue${type}List", + listOf(), + List::class.java + ) as List + if (list.isNotEmpty()) { + list.reversed().forEach { id -> + subMap[id]?.let { returnArray.add(it) } + } + subMap.values.forEach { + if (!returnArray.contains(it)) returnArray.add(it) + } + } else { returnArray.addAll(subMap.values) - returnMap["current$type"] = returnArray - return } - @Suppress("UNCHECKED_CAST") - val list = PrefManager.getNullableCustomVal( - "continueAnimeList", - listOf(), - List::class.java - ) as List - if (list.isNotEmpty()) { - list.reversed().forEach { - if (subMap.containsKey(it)) returnArray.add(subMap[it]!!) - } - for (i in subMap) { - if (i.value !in returnArray) returnArray.add(i.value) - } - } else returnArray.addAll(subMap.values) returnMap["current$type"] = returnArray - } - fun planned(type: String) { - val subMap = mutableMapOf() - val returnArray = arrayListOf() - val current = - if (type == "Anime") response?.data?.plannedAnime else response?.data?.plannedManga - current?.lists?.forEach { li -> - li.entries?.reversed()?.forEach { - val m = Media(it) - if (m.id !in removeList && if (hidePrivate) !m.isListPrivate else true) { - m.cameFromContinue = true - subMap[m.id] = m - } else { - removedMedia.add(m) - } - } - } - @Suppress("UNCHECKED_CAST") - val list = PrefManager.getNullableCustomVal( - "continueAnimeList", - listOf(), - List::class.java - ) as List - if (list.isNotEmpty()) { - list.reversed().forEach { - if (subMap.containsKey(it)) returnArray.add(subMap[it]!!) - } - for (i in subMap) { - if (i.value !in returnArray) returnArray.add(i.value) - } - } else returnArray.addAll(subMap.values) - returnMap["planned$type"] = returnArray - } + if (toShow.getOrNull(0) == true) processMedia( + "Anime", + response?.data?.currentAnime?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(), + response?.data?.repeatingAnime?.lists?.flatMap { it.entries ?: emptyList() }?.reversed() + ) + if (toShow.getOrNull(2) == true) processMedia( + "AnimePlanned", + response?.data?.plannedAnime?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(), + null + ) + if (toShow.getOrNull(3) == true) processMedia( + "Manga", + response?.data?.currentManga?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(), + response?.data?.repeatingManga?.lists?.flatMap { it.entries ?: emptyList() }?.reversed() + ) + if (toShow.getOrNull(5) == true) processMedia( + "MangaPlanned", + response?.data?.plannedManga?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(), + null + ) - fun favorite(type: String) { - val favourites = - if (type == "Anime") response?.data?.favoriteAnime?.favourites else response?.data?.favoriteManga?.favourites - val apiMediaList = if (type == "Anime") favourites?.anime else favourites?.manga + fun processFavorites(type: String, favorites: List?) { val returnArray = arrayListOf() - apiMediaList?.edges?.forEach { - it.node?.let { i -> - val m = Media(i).apply { isFav = true } - if (m.id !in removeList && if (hidePrivate) !m.isListPrivate else true) { - returnArray.add(m) + favorites?.forEach { edge -> + edge.node?.let { + val media = Media(it).apply { isFav = true } + if (media.id !in removeList && (!hidePrivate || !media.isListPrivate)) { + returnArray.add(media) } else { - removedMedia.add(m) + removedMedia.add(media) } } } returnMap["favorite$type"] = returnArray } - if (toShow.getOrNull(0) == true) { - current("Anime") - } - if (toShow.getOrNull(1) == true) { - favorite("Anime") - } - if (toShow.getOrNull(2) == true) { - planned("Anime") - } - if (toShow.getOrNull(3) == true) { - current("Manga") - } - if (toShow.getOrNull(4) == true) { - favorite("Manga") - } - if (toShow.getOrNull(5) == true) { - planned("Manga") - } + if (toShow.getOrNull(1) == true) processFavorites( + "Anime", + response?.data?.favoriteAnime?.favourites?.anime?.edges + ) + if (toShow.getOrNull(4) == true) processFavorites( + "Manga", + response?.data?.favoriteManga?.favourites?.manga?.edges + ) + if (toShow.getOrNull(6) == true) { val subMap = mutableMapOf() - response?.data?.recommendationQuery?.apply { - recommendations?.onEach { - val json = it.mediaRecommendation - if (json != null) { - val m = Media(json) - m.relation = json.type?.toString() - subMap[m.id] = m - } + response?.data?.recommendationQuery?.recommendations?.forEach { + it.mediaRecommendation?.let { json -> + val media = Media(json) + media.relation = json.type?.toString() + subMap[media.id] = media } } - response?.data?.recommendationPlannedQueryAnime?.apply { - lists?.forEach { li -> - li.entries?.forEach { - val m = Media(it) - if (m.status == "RELEASING" || m.status == "FINISHED") { - m.relation = it.media?.type?.toString() - subMap[m.id] = m - } - } + response?.data?.recommendationPlannedQueryAnime?.lists?.flatMap { + it.entries ?: emptyList() + }?.forEach { + val media = Media(it) + if (media.status in listOf("RELEASING", "FINISHED")) { + media.relation = it.media?.type?.toString() + subMap[media.id] = media } } - response?.data?.recommendationPlannedQueryManga?.apply { - lists?.forEach { li -> - li.entries?.forEach { - val m = Media(it) - if (m.status == "RELEASING" || m.status == "FINISHED") { - m.relation = it.media?.type?.toString() - subMap[m.id] = m - } - } + response?.data?.recommendationPlannedQueryManga?.lists?.flatMap { + it.entries ?: emptyList() + }?.forEach { + val media = Media(it) + if (media.status in listOf("RELEASING", "FINISHED")) { + media.relation = it.media?.type?.toString() + subMap[media.id] = media } } - val list = ArrayList(subMap.values.toList()) - list.sortByDescending { it.meanScore } + val list = ArrayList(subMap.values).apply { sortByDescending { it.meanScore } } returnMap["recommendations"] = list } @@ -1061,102 +1031,97 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: media1?.media?.mapTo(combinedList) { Media(it) } return combinedList } - private fun getPreference(pref: PrefName): Boolean = PrefManager.getVal(pref) - private fun buildQueryString(sort: String, type: String, format: String? = null, country: String? = null): String { - val includeList = if (type == "ANIME" && !getPreference(PrefName.IncludeAnimeList)) "onList:false" else if (type == "MANGA" && !getPreference(PrefName.IncludeMangaList)) "onList:false" else "" - val isAdult = if (getPreference(PrefName.AdultOnly)) "isAdult:true" else "" - val formatFilter = format?.let { "format: $it, " } ?: "" - val countryFilter = country?.let { "countryOfOrigin: $it, " } ?: "" - return """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} + private fun getPreference(pref: PrefName): Boolean = PrefManager.getVal(pref) + + private fun buildQueryString( + sort: String, + type: String, + format: String? = null, + country: String? = null + ): String { + val includeList = when { + type == "ANIME" && !getPreference(PrefName.IncludeAnimeList) -> "onList:false" + type == "MANGA" && !getPreference(PrefName.IncludeMangaList) -> "onList:false" + else -> "" + } + val isAdult = if (getPreference(PrefName.AdultOnly)) "isAdult:true" else "" + val formatFilter = format?.let { "format:$it, " } ?: "" + 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}}}""") } - }""" } private fun recentAnimeUpdates(page: Int): String { val currentTime = System.currentTimeMillis() / 1000 - return """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} - } + 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}}}}""") } - }""" - } - private fun queryAnimeList(): String { - return """{ - 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 """{ - 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")} - - }""" } - suspend fun loadAnimeList(): Map> { + 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")}}""") + } + } + + 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")}}""") + } + } + + suspend fun loadAnimeList(): Map> = coroutineScope { val list = mutableMapOf>() - fun filterRecentUpdates( - page: Page?, - ): ArrayList { - val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly) - val adultOnly: Boolean = PrefManager.getVal(PrefName.AdultOnly) - val idArr = mutableListOf() + fun filterRecentUpdates(page: Page?): ArrayList { + val listOnly = getPreference(PrefName.RecentlyListOnly) + val adultOnly = getPreference(PrefName.AdultOnly) + val idArr = mutableSetOf() return page?.airingSchedules?.mapNotNull { i -> - i.media?.let { - if (!idArr.contains(it.id)) { - val shouldAdd = when { - !listOnly && it.countryOfOrigin == "JP" && Anilist.adult && adultOnly && it.isAdult == true -> true - !listOnly && !adultOnly && it.countryOfOrigin == "JP" && it.isAdult == false -> true - listOnly && it.mediaListEntry != null -> true - else -> false - } - if (shouldAdd) { - idArr.add(it.id) - Media(it) - } else null + i.media?.takeIf { !idArr.contains(it.id) }?.let { + val shouldAdd = when { + !listOnly && it.countryOfOrigin == "JP" && adultOnly && it.isAdult == true -> true + !listOnly && !adultOnly && it.countryOfOrigin == "JP" && it.isAdult == false -> true + listOnly && it.mediaListEntry != null -> true + else -> false + } + if (shouldAdd) { + idArr.add(it.id) + Media(it) } else null } }?.toCollection(ArrayList()) ?: arrayListOf() } - executeQuery(queryAnimeList(), force = true)?.data?.apply { + + val animeList = async { executeQuery(queryAnimeList(), force = true) } + + animeList.await()?.data?.apply { list["recentUpdates"] = filterRecentUpdates(recentUpdates) list["trendingMovies"] = mediaList(trendingMovies) list["topRated"] = mediaList(topRated) list["mostFav"] = mediaList(mostFav) } - return list + + list } - suspend fun loadMangaList(): Map> { + + suspend fun loadMangaList(): Map> = coroutineScope { val list = mutableMapOf>() - executeQuery(queryMangaList(), force = true)?.data?.apply { + + val mangaList = async { executeQuery(queryMangaList(), force = true) } + + mangaList.await()?.data?.apply { list["trendingManga"] = mediaList(trendingManga) list["trendingManhwa"] = mediaList(trendingManhwa) list["trendingNovel"] = mediaList(trendingNovel) list["topRated"] = mediaList(topRated) list["mostFav"] = mediaList(mostFav) } - return list + + list } @@ -1560,10 +1525,10 @@ Page(page:$page,perPage:50) { resetNotification: Boolean = true, type: Boolean? = null ): NotificationResponse? { - val type_in = "type_in:[AIRING,MEDIA_MERGE,MEDIA_DELETION,MEDIA_DATA_CHANGE]" + val typeIn = "type_in:[AIRING,MEDIA_MERGE,MEDIA_DELETION,MEDIA_DATA_CHANGE]" val reset = if (resetNotification) "true" else "false" val res = executeQuery( - """{User(id:$id){unreadNotificationCount}Page(page:$page,perPage:$ITEMS_PER_PAGE){pageInfo{currentPage,hasNextPage}notifications(resetNotificationCount:$reset , ${if (type == true) type_in 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){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,}}}}""", force = true ) if (res != null && resetNotification) { diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt index e75383a1..f1db7ff6 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt @@ -91,17 +91,16 @@ class AnilistHomeViewModel : ViewModel() { fun getHidden(): LiveData> = hidden - @Suppress("UNCHECKED_CAST") suspend fun initHomePage() { val res = Anilist.query.initHomePage() - res["currentAnime"]?.let { animeContinue.postValue(it as ArrayList?) } - res["favoriteAnime"]?.let { animeFav.postValue(it as ArrayList?) } - res["plannedAnime"]?.let { animePlanned.postValue(it as ArrayList?) } - res["currentManga"]?.let { mangaContinue.postValue(it as ArrayList?) } - res["favoriteManga"]?.let { mangaFav.postValue(it as ArrayList?) } - res["plannedManga"]?.let { mangaPlanned.postValue(it as ArrayList?) } - res["recommendations"]?.let { recommendation.postValue(it as ArrayList?) } - res["hidden"]?.let { hidden.postValue(it as ArrayList?) } + res["currentAnime"]?.let { animeContinue.postValue(it) } + res["favoriteAnime"]?.let { animeFav.postValue(it) } + res["currentAnimePlanned"]?.let { animePlanned.postValue(it) } + res["currentManga"]?.let { mangaContinue.postValue(it) } + res["favoriteManga"]?.let { mangaFav.postValue(it) } + res["currentMangaPlanned"]?.let { mangaPlanned.postValue(it) } + res["recommendations"]?.let { recommendation.postValue(it) } + res["hidden"]?.let { hidden.postValue(it) } } suspend fun loadMain(context: FragmentActivity) { diff --git a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt index 4452a2df..ed3cb02c 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt @@ -32,6 +32,7 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STAR import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER import ani.dantotsu.snackString import ani.dantotsu.util.Logger +import ani.dantotsu.util.NumberConverter.Companion.ofLength import com.anggrayudi.storage.file.deleteRecursively import com.anggrayudi.storage.file.forceDelete import com.anggrayudi.storage.file.openOutputStream @@ -235,7 +236,7 @@ class MangaDownloaderService : Service() { } if (bitmap != null) { - saveToDisk("$index.jpg", outputDir, bitmap) + saveToDisk("${index.ofLength(3)}.jpg", outputDir, bitmap) } farthest++ diff --git a/app/src/main/java/ani/dantotsu/home/HomeFragment.kt b/app/src/main/java/ani/dantotsu/home/HomeFragment.kt index 43bf8bea..dfd39e6a 100644 --- a/app/src/main/java/ani/dantotsu/home/HomeFragment.kt +++ b/app/src/main/java/ani/dantotsu/home/HomeFragment.kt @@ -50,6 +50,7 @@ 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 import kotlinx.coroutines.withContext import kotlin.math.max @@ -457,53 +458,56 @@ class HomeFragment : Fragment() { var running = false val live = Refresh.activity.getOrPut(1) { MutableLiveData(true) } - live.observe(viewLifecycleOwner) - { - if (!running && it) { + live.observe(viewLifecycleOwner) { shouldRefresh -> + if (!running && shouldRefresh) { running = true scope.launch { withContext(Dispatchers.IO) { - //Get userData First - Anilist.userid = - PrefManager.getNullableVal(PrefName.AnilistUserId, null) - ?.toIntOrNull() + // Get user data first + Anilist.userid = PrefManager.getNullableVal(PrefName.AnilistUserId, null)?.toIntOrNull() if (Anilist.userid == null) { - getUserId(requireContext()) { - load() - } - } else { - CoroutineScope(Dispatchers.IO).launch { + withContext(Dispatchers.Main) { getUserId(requireContext()) { load() } } + } else { + getUserId(requireContext()) { + load() + } } model.loaded = true - CoroutineScope(Dispatchers.IO).launch { - model.setListImages() - } + model.setListImages() + } - var empty = true - val homeLayoutShow: List = - PrefManager.getVal(PrefName.HomeLayout) - model.initHomePage() - model.initUserStatus() - (array.indices).forEach { i -> + var empty = true + val homeLayoutShow: List = PrefManager.getVal(PrefName.HomeLayout) + + withContext(Dispatchers.Main) { + homeLayoutShow.indices.forEach { i -> if (homeLayoutShow.elementAt(i)) { empty = false - } else withContext(Dispatchers.Main) { + } else { containers[i].visibility = View.GONE } } - model.empty.postValue(empty) } + + val initHomePage = async(Dispatchers.IO) { model.initHomePage() } + val initUserStatus = async(Dispatchers.IO) { model.initUserStatus() } + initHomePage.await() + initUserStatus.await() + + withContext(Dispatchers.Main) { + model.empty.postValue(empty) + binding.homeHiddenItemsContainer.visibility = View.GONE + } + live.postValue(false) _binding?.homeRefresh?.isRefreshing = false running = false } - binding.homeHiddenItemsContainer.visibility = View.GONE } - } } diff --git a/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt b/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt index 7fc0e78b..ced8b2d8 100644 --- a/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt +++ b/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt @@ -49,7 +49,10 @@ class StatusActivity : AppCompatActivity(), StoriesCallback { if (activity.getOrNull(position) != null) { val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity ) val startIndex = if ( startFrom > 0) startFrom else 0 - binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1) + binding.stories.setStoriesList( + activityList = activity[position].activity, + startIndex = startIndex + 1 + ) } else { Logger.log("index out of bounds for position $position of size ${activity.size}") finish() @@ -92,7 +95,7 @@ class StatusActivity : AppCompatActivity(), StoriesCallback { val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity ) val startIndex= if ( startFrom > 0) startFrom else 0 binding.stories.startAnimation(slideOutLeft) - binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1) + binding.stories.setStoriesList(activity[position].activity, startIndex + 1) binding.stories.startAnimation(slideInRight) } else { finish() @@ -107,7 +110,7 @@ class StatusActivity : AppCompatActivity(), StoriesCallback { val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity ) val startIndex = if ( startFrom > 0) startFrom else 0 binding.stories.startAnimation(slideOutRight) - binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1) + binding.stories.setStoriesList(activity[position].activity,startIndex + 1) binding.stories.startAnimation(slideInLeft) } else { finish() diff --git a/app/src/main/java/ani/dantotsu/home/status/Stories.kt b/app/src/main/java/ani/dantotsu/home/status/Stories.kt index a017b7ab..a141a5d6 100644 --- a/app/src/main/java/ani/dantotsu/home/status/Stories.kt +++ b/app/src/main/java/ani/dantotsu/home/status/Stories.kt @@ -49,7 +49,6 @@ import kotlin.math.abs class Stories @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr), View.OnTouchListener { - private lateinit var activity: FragmentActivity private lateinit var binding: FragmentStatusBinding private lateinit var activityList: List private lateinit var storiesListener: StoriesCallback @@ -80,10 +79,9 @@ class Stories @JvmOverloads constructor( fun setStoriesList( - activityList: List, activity: FragmentActivity, startIndex: Int = 1 + activityList: List, startIndex: Int = 1 ) { this.activityList = activityList - this.activity = activity this.storyIndex = startIndex addLoadingViews(activityList) } @@ -368,7 +366,9 @@ class Stories @JvmOverloads constructor( if ( story.status?.contains("completed") == false && !story.status.contains("plans") && - !story.status.contains("repeating") + !story.status.contains("repeating")&& + !story.status.contains("paused")&& + !story.status.contains("dropped") ) { "of ${story.media?.title?.userPreferred}" } else { @@ -389,7 +389,7 @@ class Stories @JvmOverloads constructor( story.media?.id ), ActivityOptionsCompat.makeSceneTransitionAnimation( - activity, + (it.context as FragmentActivity), binding.coverImage, ViewCompat.getTransitionName(binding.coverImage)!! ).toBundle() @@ -427,7 +427,7 @@ class Stories @JvmOverloads constructor( binding.activityReplies.setColorFilter(ContextCompat.getColor(context, R.color.bg_opp)) binding.activityRepliesContainer.setOnClickListener { RepliesBottomDialog.newInstance(story.id) - .show(activity.supportFragmentManager, "replies") + .show((it.context as FragmentActivity).supportFragmentManager, "replies") } binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor) binding.activityLikeCount.text = story.likeCount.toString() @@ -435,10 +435,9 @@ class Stories @JvmOverloads constructor( like() } binding.activityLikeContainer.setOnLongClickListener { - val context = activity UsersDialogFragment().apply { userList(userList) - show(context.supportFragmentManager, "dialog") + show((it.context as FragmentActivity).supportFragmentManager, "dialog") } true } diff --git a/app/src/main/java/ani/dantotsu/profile/activity/ActivityFragment.kt b/app/src/main/java/ani/dantotsu/profile/activity/ActivityFragment.kt index 3b9b32d0..8b1500bd 100644 --- a/app/src/main/java/ani/dantotsu/profile/activity/ActivityFragment.kt +++ b/app/src/main/java/ani/dantotsu/profile/activity/ActivityFragment.kt @@ -60,7 +60,7 @@ class ActivityFragment : Fragment() { binding.feedRefresh.updateLayoutParams { bottomMargin = navBarHeight } - binding.emptyTextView.text = getString(R.string.no_activities) + binding.emptyTextView.text = getString(R.string.nothing_here) lifecycleScope.launch { getList() if (adapter.itemCount == 0) { diff --git a/app/src/main/java/ani/dantotsu/profile/activity/FeedActivity.kt b/app/src/main/java/ani/dantotsu/profile/activity/FeedActivity.kt index 45122fec..40cb8f96 100644 --- a/app/src/main/java/ani/dantotsu/profile/activity/FeedActivity.kt +++ b/app/src/main/java/ani/dantotsu/profile/activity/FeedActivity.kt @@ -51,6 +51,7 @@ class FeedActivity : AppCompatActivity() { binding.notificationBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() } val getOne = intent.getIntExtra("activityId", -1) if (getOne != -1) { navBar.visibility = View.GONE } + binding.notificationViewPager.isUserInputEnabled = false binding.notificationViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, getOne) binding.notificationViewPager.setOffscreenPageLimit(4) binding.notificationViewPager.setCurrentItem(selected, false) @@ -63,13 +64,7 @@ class FeedActivity : AppCompatActivity() { newTab: AnimatedBottomBar.Tab ) { selected = newIndex - binding.notificationViewPager.setCurrentItem(selected, true) - } - }) - binding.notificationViewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - super.onPageSelected(position) - navBar.selectTabAt(position) + binding.notificationViewPager.setCurrentItem(selected, false) } }) } diff --git a/app/src/main/java/ani/dantotsu/profile/notification/NotificationActivity.kt b/app/src/main/java/ani/dantotsu/profile/notification/NotificationActivity.kt index 75089031..bd5fad0b 100644 --- a/app/src/main/java/ani/dantotsu/profile/notification/NotificationActivity.kt +++ b/app/src/main/java/ani/dantotsu/profile/notification/NotificationActivity.kt @@ -16,7 +16,8 @@ import ani.dantotsu.initActivity import ani.dantotsu.navBarHeight import ani.dantotsu.statusBarHeight import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType +import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.* +import ani.dantotsu.profile.notification.NotificationFragment.Companion.newInstance import nl.joery.animatedbottombar.AnimatedBottomBar class NotificationActivity : AppCompatActivity() { @@ -48,6 +49,7 @@ class NotificationActivity : AppCompatActivity() { binding.notificationBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() } val getOne = intent.getIntExtra("activityId", -1) if (getOne != -1) navBar.isVisible = false + binding.notificationViewPager.isUserInputEnabled = false binding.notificationViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, getOne) binding.notificationViewPager.setCurrentItem(selected, false) navBar.selectTabAt(selected) @@ -59,13 +61,7 @@ class NotificationActivity : AppCompatActivity() { newTab: AnimatedBottomBar.Tab ) { selected = newIndex - binding.notificationViewPager.setCurrentItem(selected, true) - } - }) - binding.notificationViewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - super.onPageSelected(position) - navBar.selectTabAt(position) + binding.notificationViewPager.setCurrentItem(selected, false) } }) } @@ -83,11 +79,11 @@ class NotificationActivity : AppCompatActivity() { override fun getItemCount(): Int = if (id != -1) 1 else 4 override fun createFragment(position: Int): Fragment = when (position) { - 0 -> NotificationFragment.newInstance(NotificationType.USER) - 1 -> NotificationFragment.newInstance(if (id != -1) NotificationType.ONE else NotificationType.MEDIA, id) - 2 -> NotificationFragment.newInstance(NotificationType.SUBSCRIPTION) - 3 -> NotificationFragment.newInstance(NotificationType.COMMENT) - else -> NotificationFragment.newInstance(NotificationType.MEDIA) + 0 -> newInstance(USER) + 1 -> newInstance(if (id != -1) ONE else MEDIA, id) + 2 -> newInstance(SUBSCRIPTION) + 3 -> newInstance(COMMENT) + else -> newInstance(MEDIA) } } } diff --git a/app/src/main/java/ani/dantotsu/profile/notification/NotificationFragment.kt b/app/src/main/java/ani/dantotsu/profile/notification/NotificationFragment.kt index 6360763d..c38ec0fb 100644 --- a/app/src/main/java/ani/dantotsu/profile/notification/NotificationFragment.kt +++ b/app/src/main/java/ani/dantotsu/profile/notification/NotificationFragment.kt @@ -20,7 +20,11 @@ import ani.dantotsu.notifications.comment.CommentStore import ani.dantotsu.notifications.subscription.SubscriptionStore import ani.dantotsu.profile.ProfileActivity import ani.dantotsu.profile.activity.FeedActivity -import ani.dantotsu.setBaseline +import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.COMMENT +import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.MEDIA +import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.ONE +import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.SUBSCRIPTION +import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.USER import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName import com.xwray.groupie.GroupieAdapter @@ -29,8 +33,8 @@ import kotlinx.coroutines.launch class NotificationFragment : Fragment() { - private lateinit var type : NotificationType - private var getID : Int = -1 + private lateinit var type: NotificationType + private var getID: Int = -1 private lateinit var binding: FragmentNotificationsBinding private var adapter: GroupieAdapter = GroupieAdapter() private var currentPage = 1 @@ -53,12 +57,10 @@ class NotificationFragment : Fragment() { binding.notificationRecyclerView.adapter = adapter binding.notificationRecyclerView.layoutManager = LinearLayoutManager(context) binding.notificationProgressBar.isVisible = true - binding.emptyTextView.text = getString(R.string.no_notifications) + binding.emptyTextView.text = getString(R.string.nothing_here) lifecycleScope.launch { getList() - if (adapter.itemCount == 0) { - binding.emptyTextView.isVisible = true - } + binding.notificationProgressBar.isVisible = false } binding.notificationSwipeRefresh.setOnRefreshListener { @@ -87,13 +89,16 @@ class NotificationFragment : Fragment() { private suspend fun getList() { val list = when (type) { - NotificationType.ONE -> getNotificationsFiltered(false) { it.id == getID } - NotificationType.MEDIA -> getNotificationsFiltered(type = true) { it.media != null } - NotificationType.USER -> getNotificationsFiltered { it.media == null } - NotificationType.SUBSCRIPTION -> getSubscriptions() - NotificationType.COMMENT -> getComments() + ONE -> getNotificationsFiltered(false) { it.id == getID } + MEDIA -> getNotificationsFiltered(type = true) { it.media != null } + USER -> getNotificationsFiltered { it.media == null } + SUBSCRIPTION -> getSubscriptions() + COMMENT -> getComments() + } + adapter.addAll(list.map { NotificationItem(it, type, adapter, ::onClick) }) + if (adapter.itemCount == 0) { + binding.emptyTextView.isVisible = true } - adapter.addAll(list.map { NotificationItem(it, ::onClick) }) } private suspend fun getNotificationsFiltered( @@ -114,8 +119,11 @@ class NotificationFragment : Fragment() { PrefName.SubscriptionNotificationStore, null ) ?: listOf() - return list.sortedByDescending { (it.time / 1000L).toInt() } - .filter { it.image != null }.map { + + return list + .sortedByDescending { (it.time / 1000L).toInt() } + .filter { it.image != null } // to remove old data + .map { Notification( it.type, System.currentTimeMillis().toInt(), @@ -162,19 +170,31 @@ class NotificationFragment : Fragment() { fun onClick(id: Int, optional: Int?, type: NotificationClickType) { val intent = when (type) { - NotificationClickType.USER -> Intent(requireContext(), ProfileActivity::class.java).apply { + NotificationClickType.USER -> Intent( + requireContext(), + ProfileActivity::class.java + ).apply { putExtra("userId", id) } - NotificationClickType.MEDIA -> Intent(requireContext(), MediaDetailsActivity::class.java).apply { + NotificationClickType.MEDIA -> Intent( + requireContext(), + MediaDetailsActivity::class.java + ).apply { putExtra("mediaId", id) } - NotificationClickType.ACTIVITY -> Intent(requireContext(), FeedActivity::class.java).apply { + NotificationClickType.ACTIVITY -> Intent( + requireContext(), + FeedActivity::class.java + ).apply { putExtra("activityId", id) } - NotificationClickType.COMMENT -> Intent(requireContext(), MediaDetailsActivity::class.java).apply { + NotificationClickType.COMMENT -> Intent( + requireContext(), + MediaDetailsActivity::class.java + ).apply { putExtra("FRAGMENT_TO_LOAD", "COMMENTS") putExtra("mediaId", id) putExtra("commentId", optional ?: -1) @@ -188,6 +208,7 @@ class NotificationFragment : Fragment() { } } + override fun onResume() { super.onResume() if (this::binding.isInitialized) { diff --git a/app/src/main/java/ani/dantotsu/profile/notification/NotificationItem.kt b/app/src/main/java/ani/dantotsu/profile/notification/NotificationItem.kt index f51225ea..61273374 100644 --- a/app/src/main/java/ani/dantotsu/profile/notification/NotificationItem.kt +++ b/app/src/main/java/ani/dantotsu/profile/notification/NotificationItem.kt @@ -8,16 +8,27 @@ import ani.dantotsu.connections.anilist.api.Notification import ani.dantotsu.connections.anilist.api.NotificationType import ani.dantotsu.databinding.ItemNotificationBinding import ani.dantotsu.loadImage -import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationClickType +import ani.dantotsu.notifications.comment.CommentStore +import ani.dantotsu.notifications.subscription.SubscriptionStore import ani.dantotsu.profile.activity.ActivityItemBuilder +import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationClickType +import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.COMMENT +import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.SUBSCRIPTION import ani.dantotsu.setAnimation +import ani.dantotsu.settings.saving.PrefManager +import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.toPx +import ani.dantotsu.util.customAlertDialog +import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.viewbinding.BindableItem class NotificationItem( private val notification: Notification, - val clickCallback: (Int, Int?, NotificationClickType) -> Unit -) : BindableItem() { + val type: NotificationFragment.Companion.NotificationType, + val parentAdapter: GroupieAdapter, + val clickCallback: (Int, Int?, NotificationClickType) -> Unit, + + ) : BindableItem() { private lateinit var binding: ItemNotificationBinding override fun bind(viewBinding: ItemNotificationBinding, position: Int) { binding = viewBinding @@ -25,6 +36,48 @@ class NotificationItem( setBinding() } + fun dialog() { + when (type) { + COMMENT, SUBSCRIPTION -> { + binding.root.context.customAlertDialog().apply { + setTitle(R.string.delete) + setMessage(ActivityItemBuilder.getContent(notification)) + setPosButton(R.string.yes) { + when (type) { + COMMENT -> { + val list = PrefManager.getNullableVal>( + PrefName.CommentNotificationStore, + null + ) ?: listOf() + val newList = list.filter { it.commentId != notification.commentId } + PrefManager.setVal(PrefName.CommentNotificationStore, newList) + parentAdapter.remove(this@NotificationItem) + + } + + SUBSCRIPTION -> { + val list = PrefManager.getNullableVal>( + PrefName.SubscriptionNotificationStore, + null + ) ?: listOf() + val newList = list.filter { (it.time / 1000L).toInt() != notification.createdAt} + PrefManager.setVal(PrefName.SubscriptionNotificationStore, newList) + parentAdapter.remove(this@NotificationItem) + } + + else -> {} + } + } + setNegButton(R.string.no) + show() + } + } + + else -> {} + } + + } + override fun getLayout(): Int { return R.layout.item_notification } @@ -33,7 +86,11 @@ class NotificationItem( return ItemNotificationBinding.bind(view) } - private fun image(user: Boolean = false, commentNotification: Boolean = false, newRelease: Boolean = false) { + private fun image( + user: Boolean = false, + commentNotification: Boolean = false, + newRelease: Boolean = false + ) { val cover = if (user) notification.user?.bannerImage ?: notification.user?.avatar?.medium else notification.media?.bannerImage @@ -348,6 +405,14 @@ class NotificationItem( } } } + binding.notificationCoverUser.setOnLongClickListener { + dialog() + true + } + binding.notificationBannerImage.setOnLongClickListener { + dialog() + true + } } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/SubscriptionItem.kt b/app/src/main/java/ani/dantotsu/settings/SubscriptionItem.kt index bb8c0396..bb60cd4e 100644 --- a/app/src/main/java/ani/dantotsu/settings/SubscriptionItem.kt +++ b/app/src/main/java/ani/dantotsu/settings/SubscriptionItem.kt @@ -9,31 +9,25 @@ import ani.dantotsu.loadImage import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.notifications.subscription.SubscriptionHelper import com.xwray.groupie.GroupieAdapter -import com.xwray.groupie.Item import com.xwray.groupie.viewbinding.BindableItem class SubscriptionItem( val id: Int, private val media: SubscriptionHelper.Companion.SubscribeMedia, - private val adapter: GroupieAdapter + private val adapter: GroupieAdapter, + private val onItemRemoved: (Int) -> Unit ) : BindableItem() { private lateinit var binding: ItemSubscriptionBinding - override fun bind(p0: ItemSubscriptionBinding, p1: Int) { - val context = p0.root.context - binding = p0 - val parserName = if (media.isAnime) - SubscriptionHelper.getAnimeParser(media.id).name - else - SubscriptionHelper.getMangaParser(media.id).name - val mediaName = media.name - val showName = "$mediaName ($parserName)" - binding.subscriptionName.text = showName + + override fun bind(viewBinding: ItemSubscriptionBinding, position: Int) { + binding = viewBinding + val context = binding.root.context + + binding.subscriptionName.text = media.name binding.root.setOnClickListener { ContextCompat.startActivity( context, - Intent(context, MediaDetailsActivity::class.java).putExtra( - "mediaId", media.id - ), + Intent(context, MediaDetailsActivity::class.java).putExtra("mediaId", media.id), null ) } @@ -41,14 +35,11 @@ class SubscriptionItem( binding.deleteSubscription.setOnClickListener { SubscriptionHelper.deleteSubscription(id, true) adapter.remove(this) + onItemRemoved(id) } } - override fun getLayout(): Int { - return R.layout.item_subscription - } + override fun getLayout(): Int = R.layout.item_subscription - override fun initializeViewBinding(p0: View): ItemSubscriptionBinding { - return ItemSubscriptionBinding.bind(p0) - } + override fun initializeViewBinding(view: View): ItemSubscriptionBinding = ItemSubscriptionBinding.bind(view) } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/SubscriptionSource.kt b/app/src/main/java/ani/dantotsu/settings/SubscriptionSource.kt new file mode 100644 index 00000000..80975159 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/SubscriptionSource.kt @@ -0,0 +1,113 @@ +package ani.dantotsu.settings + +import android.app.AlertDialog +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.View +import android.view.ViewGroup +import ani.dantotsu.R +import ani.dantotsu.databinding.ItemExtensionBinding +import ani.dantotsu.notifications.subscription.SubscriptionHelper +import com.xwray.groupie.GroupieAdapter +import com.xwray.groupie.viewbinding.BindableItem + +class SubscriptionSource( + private val parserName: String, + private val subscriptions: MutableList, + private val adapter: GroupieAdapter, + private var parserIcon: Drawable? = null, + private val onGroupRemoved: (SubscriptionSource) -> Unit +) : BindableItem() { + private lateinit var binding: ItemExtensionBinding + private var isExpanded = false + + override fun bind(viewBinding: ItemExtensionBinding, position: Int) { + binding = viewBinding + binding.extensionNameTextView.text = parserName + updateSubscriptionCount() + binding.extensionSubscriptions.visibility = View.VISIBLE + + binding.extensionSubscriptions.setOnClickListener(null) + binding.root.setOnClickListener { + isExpanded = !isExpanded + toggleSubscriptions() + } + binding.subscriptionCount.setOnClickListener { + showRemoveAllSubscriptionsDialog(it.context) + } + binding.extensionIconImageView.visibility = View.VISIBLE + val layoutParams = binding.extensionIconImageView.layoutParams as ViewGroup.MarginLayoutParams + layoutParams.leftMargin = 28 + binding.extensionIconImageView.layoutParams = layoutParams + + parserIcon?.let { + binding.extensionIconImageView.setImageDrawable(it) + } ?: run { + binding.extensionIconImageView.setImageResource(R.drawable.control_background_40dp) + } + + binding.extensionPinImageView.visibility = View.GONE + binding.extensionVersionTextView.visibility = View.GONE + binding.closeTextView.visibility = View.GONE + binding.settingsImageView.visibility = View.GONE + } + + private fun updateSubscriptionCount() { + binding.subscriptionCount.text = subscriptions.size.toString() + binding.subscriptionCount.visibility = if (subscriptions.isEmpty()) View.GONE else View.VISIBLE + } + + private fun showRemoveAllSubscriptionsDialog(context: Context) { + AlertDialog.Builder(context, R.style.MyPopup) + .setTitle(R.string.remove_all_subscriptions) + .setMessage(context.getString(R.string.remove_all_subscriptions_desc, parserName)) + .setPositiveButton(R.string.apply) { _, _ -> + removeAllSubscriptions() + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + private fun removeAllSubscriptions() { + subscriptions.forEach { subscription -> + SubscriptionHelper.deleteSubscription(subscription.id, false) + } + if (isExpanded) { + val startPosition = adapter.getAdapterPosition(this) + 1 + repeat(subscriptions.size) { + adapter.removeGroupAtAdapterPosition(startPosition) + } + } + subscriptions.clear() + onGroupRemoved(this) + } + + private fun removeSubscription(id: Any?) { + subscriptions.removeAll { it.id == id } + updateSubscriptionCount() + if (subscriptions.isEmpty()) { + onGroupRemoved(this) + } else { + adapter.notifyItemChanged(adapter.getAdapterPosition(this)) + } + } + + private fun toggleSubscriptions() { + val startPosition = adapter.getAdapterPosition(this) + 1 + if (isExpanded) { + subscriptions.forEachIndexed { index, subscribeMedia -> + adapter.add(startPosition + index, SubscriptionItem(subscribeMedia.id, subscribeMedia, adapter) { removedId -> + removeSubscription(removedId) + }) + } + } else { + repeat(subscriptions.size) { + adapter.removeGroupAtAdapterPosition(startPosition) + } + } + } + + override fun getLayout(): Int = R.layout.item_extension + + override fun initializeViewBinding(view: View): ItemExtensionBinding = ItemExtensionBinding.bind(view) +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/SubscriptionsBottomDialog.kt b/app/src/main/java/ani/dantotsu/settings/SubscriptionsBottomDialog.kt index 2b833114..93fd58db 100644 --- a/app/src/main/java/ani/dantotsu/settings/SubscriptionsBottomDialog.kt +++ b/app/src/main/java/ani/dantotsu/settings/SubscriptionsBottomDialog.kt @@ -1,5 +1,6 @@ package ani.dantotsu.settings +import android.graphics.drawable.Drawable import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -9,13 +10,21 @@ import ani.dantotsu.BottomSheetDialogFragment import ani.dantotsu.R import ani.dantotsu.databinding.BottomSheetRecyclerBinding import ani.dantotsu.notifications.subscription.SubscriptionHelper +import ani.dantotsu.parsers.novel.NovelExtensionManager import com.xwray.groupie.GroupieAdapter +import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager +import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get class SubscriptionsBottomDialog : BottomSheetDialogFragment() { private var _binding: BottomSheetRecyclerBinding? = null private val binding get() = _binding!! private val adapter: GroupieAdapter = GroupieAdapter() private var subscriptions: Map = mapOf() + private val animeExtension: AnimeExtensionManager = Injekt.get() + private val mangaExtensions: MangaExtensionManager = Injekt.get() + private val novelExtensions: NovelExtensionManager = Injekt.get() override fun onCreateView( inflater: LayoutInflater, @@ -36,8 +45,33 @@ class SubscriptionsBottomDialog : BottomSheetDialogFragment() { val context = requireContext() binding.title.text = context.getString(R.string.subscriptions) binding.replyButton.visibility = View.GONE - subscriptions.forEach { (id, media) -> - adapter.add(SubscriptionItem(id, media, adapter)) + + val groupedSubscriptions = subscriptions.values.groupBy { + if (it.isAnime) SubscriptionHelper.getAnimeParser(it.id).name + else SubscriptionHelper.getMangaParser(it.id).name + } + + groupedSubscriptions.forEach { (parserName, mediaList) -> + adapter.add(SubscriptionSource( + parserName, + mediaList.toMutableList(), + adapter, + getParserIcon(parserName) + ) { group -> + adapter.remove(group) + }) + } + } + + private fun getParserIcon(parserName: String): Drawable? { + return when { + animeExtension.installedExtensionsFlow.value.any { it.name == parserName } -> + animeExtension.installedExtensionsFlow.value.find { it.name == parserName }?.icon + mangaExtensions.installedExtensionsFlow.value.any { it.name == parserName } -> + mangaExtensions.installedExtensionsFlow.value.find { it.name == parserName }?.icon + novelExtensions.installedExtensionsFlow.value.any { it.name == parserName } -> + novelExtensions.installedExtensionsFlow.value.find { it.name == parserName }?.icon + else -> null } } diff --git a/app/src/main/java/ani/dantotsu/util/ActivityMarkdownCreator.kt b/app/src/main/java/ani/dantotsu/util/ActivityMarkdownCreator.kt index ccddfbab..fdc24937 100644 --- a/app/src/main/java/ani/dantotsu/util/ActivityMarkdownCreator.kt +++ b/app/src/main/java/ani/dantotsu/util/ActivityMarkdownCreator.kt @@ -5,7 +5,6 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.view.updateLayoutParams import androidx.core.widget.addTextChangedListener @@ -16,6 +15,7 @@ import ani.dantotsu.databinding.ActivityMarkdownCreatorBinding import ani.dantotsu.initActivity import ani.dantotsu.navBarHeight import ani.dantotsu.openLinkInBrowser +import ani.dantotsu.others.AndroidBug5497Workaround import ani.dantotsu.statusBarHeight import ani.dantotsu.themes.ThemeManager import ani.dantotsu.toast @@ -42,7 +42,7 @@ class ActivityMarkdownCreator : AppCompatActivity() { BOLD("****", 2, R.id.formatBold), ITALIC("**", 1, R.id.formatItalic), STRIKETHROUGH("~~~~", 2, R.id.formatStrikethrough), - SPOILER("||", 2, R.id.formatSpoiler), + SPOILER("~||~", 2, R.id.formatSpoiler), LINK("[Placeholder](%s)", 0, R.id.formatLink), IMAGE("img(%s)", 0, R.id.formatImage), YOUTUBE("youtube(%s)", 0, R.id.formatYoutube), @@ -68,6 +68,7 @@ class ActivityMarkdownCreator : AppCompatActivity() { bottomMargin += navBarHeight } setContentView(binding.root) + AndroidBug5497Workaround.assistActivity(this) {} val params = binding.createButton.layoutParams as ViewGroup.MarginLayoutParams params.marginEnd = 16 * resources.displayMetrics.density.toInt() diff --git a/app/src/main/java/ani/dantotsu/util/NumberConverter.kt b/app/src/main/java/ani/dantotsu/util/NumberConverter.kt index 49d41de7..61aefe4e 100644 --- a/app/src/main/java/ani/dantotsu/util/NumberConverter.kt +++ b/app/src/main/java/ani/dantotsu/util/NumberConverter.kt @@ -47,5 +47,9 @@ class NumberConverter { val intBits = java.lang.Float.floatToIntBits(number) return Integer.toBinaryString(intBits) } + + fun Int.ofLength(length: Int): String { + return this.toString().padStart(length, '0') + } } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_status.xml b/app/src/main/res/layout/fragment_status.xml index 72f488e0..63664c60 100644 --- a/app/src/main/res/layout/fragment_status.xml +++ b/app/src/main/res/layout/fragment_status.xml @@ -177,7 +177,7 @@ android:layout_height="wrap_content" android:layout_gravity="bottom" android:layout_marginHorizontal="16dp" - android:layout_marginVertical="32dp" + android:layout_marginVertical="6dp" android:orientation="horizontal"> + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index befeed04..832cc9fa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -83,8 +83,7 @@ All - No more notifications - No more activities + Nothing here Followers Write a Message STATUS @@ -417,6 +416,8 @@ Use Alarm Manager for reliable Notifications Using Alarm Manger can help fight against battery optimization, but may consume more battery. It also requires the Alarm Manager permission. Use + Remove All Subscriptions + Are you sure you want to remove all subscriptions for %1$s? Notification for Checking Subscriptions Subscriptions Update Frequency : %1$s Subscriptions Update Frequency