diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9b071c2b..c64a42af 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -132,10 +132,11 @@ - - + + + diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt b/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt index a712aba3..6b0d288c 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt @@ -15,6 +15,8 @@ import ani.dantotsu.snackString import ani.dantotsu.toast import ani.dantotsu.util.Logger import java.util.Calendar +import java.util.Locale +import kotlin.math.abs object Anilist { val query: AnilistQueries = AnilistQueries() @@ -22,7 +24,7 @@ object Anilist { var token: String? = null var username: String? = null - var adult: Boolean = false + var userid: Int? = null var avatar: String? = null var bg: String? = null @@ -36,6 +38,17 @@ object Anilist { var rateLimitReset: Long = 0 var initialized = false + var adult: Boolean = false + var titleLanguage: String? = null + var staffNameLanguage: String? = null + var airingNotifications: Boolean = false + var restrictMessagesToFollowing: Boolean = false + var scoreFormat: String? = null + var rowOrder: String? = null + var activityMergeTime: Int? = null + var timezone: String? = null + var animeCustomLists: List? = null + var mangaCustomLists: List? = null val sortBy = listOf( "SCORE_DESC", @@ -96,6 +109,86 @@ object Anilist { "Original Creator", "Story & Art", "Story" ) + val timeZone = listOf( + "(GMT-11:00) Pago Pago", + "(GMT-10:00) Hawaii Time", + "(GMT-09:00) Alaska Time", + "(GMT-08:00) Pacific Time", + "(GMT-07:00) Mountain Time", + "(GMT-06:00) Central Time", + "(GMT-05:00) Eastern Time", + "(GMT-04:00) Atlantic Time - Halifax", + "(GMT-03:00) Sao Paulo", + "(GMT-02:00) Mid-Atlantic", + "(GMT-01:00) Azores", + "(GMT+00:00) London", + "(GMT+01:00) Berlin", + "(GMT+02:00) Helsinki", + "(GMT+03:00) Istanbul", + "(GMT+04:00) Dubai", + "(GMT+04:30) Kabul", + "(GMT+05:00) Maldives", + "(GMT+05:30) India Standard Time", + "(GMT+05:45) Kathmandu", + "(GMT+06:00) Dhaka", + "(GMT+06:30) Cocos", + "(GMT+07:00) Bangkok", + "(GMT+08:00) Hong Kong", + "(GMT+08:30) Pyongyang", + "(GMT+09:00) Tokyo", + "(GMT+09:30) Central Time - Darwin", + "(GMT+10:00) Eastern Time - Brisbane", + "(GMT+10:30) Central Time - Adelaide", + "(GMT+11:00) Eastern Time - Melbourne, Sydney", + "(GMT+12:00) Nauru", + "(GMT+13:00) Auckland", + "(GMT+14:00) Kiritimati", + ) + + val titleLang = listOf( + "English (Attack on Titan)", + "Romaji (Shingeki no Kyojin)", + "Native (進撃の巨人)" + ) + + val staffNameLang = listOf( + "Romaji, Western Order (Killua Zoldyck)", + "Romaji (Zoldyck Killua)", + "Native (キルア=ゾルディック)" + ) + + val ScoreFormat = listOf( + "100 Point (55/100)", + "10 Point Decimal (5.5/10)", + "10 Point (5/10)", + "5 Star (3/5)", + "3 Point Smiley :)" + ) + + val rowOrderMap = mapOf( + "Score" to "score", + "Title" to "title", + "Last Updated" to "updatedAt", + "Last Added" to "id" + ) + + val activityMergeTimeMap = mapOf( + "Never" to 0, + "30 mins" to 30, + "69 mins" to 69, + "1 hour" to 60, + "2 hours" to 120, + "3 hours" to 180, + "6 hours" to 360, + "12 hours" to 720, + "1 day" to 1440, + "2 days" to 2880, + "3 days" to 4320, + "1 week" to 10080, + "2 weeks" to 20160, + "Always" to 29160 + ) + private val cal: Calendar = Calendar.getInstance() private val currentYear = cal.get(Calendar.YEAR) private val currentSeason: Int = when (cal.get(Calendar.MONTH)) { @@ -106,6 +199,32 @@ object Anilist { else -> 0 } + fun getDisplayTimezone(apiTimezone: String): String { + val parts = apiTimezone.split(":") + if (parts.size != 2) return "(GMT+00:00) London" + + val hours = parts[0].toIntOrNull() ?: 0 + val minutes = parts[1].toIntOrNull() ?: 0 + val sign = if (hours >= 0) "+" else "-" + val formattedHours = String.format(Locale.US, "%02d", abs(hours)) + val formattedMinutes = String.format(Locale.US, "%02d", minutes) + + val searchString = "(GMT$sign$formattedHours:$formattedMinutes)" + return timeZone.find { it.contains(searchString) } ?: "(GMT+00:00) London" + } + + fun getApiTimezone(displayTimezone: String): String { + val regex = """\(GMT([+-])(\d{2}):(\d{2})\)""".toRegex() + val matchResult = regex.find(displayTimezone) + return if (matchResult != null) { + val (sign, hours, minutes) = matchResult.destructured + val formattedSign = if (sign == "+") "" else "-" + "$formattedSign$hours:$minutes" + } else { + "00:00" + } + } + private fun getSeason(next: Boolean): Pair { var newSeason = if (next) currentSeason + 1 else currentSeason - 1 var newYear = currentYear diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistMutations.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistMutations.kt index 3d7ac85e..de0f6e2f 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistMutations.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistMutations.kt @@ -10,9 +10,92 @@ import kotlinx.serialization.json.JsonObject class AnilistMutations { + suspend fun updateSettings( + timezone: String? = null, + titleLanguage: String? = null, + staffNameLanguage: String? = null, + activityMergeTime: Int? = null, + airingNotifications: Boolean? = null, + displayAdultContent: Boolean? = null, + restrictMessagesToFollowing: Boolean? = null, + scoreFormat: String? = null, + rowOrder: String? = null, + ) { + val query = """ + mutation ( + ${"$"}timezone: String, + ${"$"}titleLanguage: UserTitleLanguage, + ${"$"}staffNameLanguage: UserStaffNameLanguage, + ${"$"}activityMergeTime: Int, + ${"$"}airingNotifications: Boolean, + ${"$"}displayAdultContent: Boolean, + ${"$"}restrictMessagesToFollowing: Boolean, + ${"$"}scoreFormat: ScoreFormat, + ${"$"}rowOrder: String + ) { + UpdateUser( + timezone: ${"$"}timezone, + titleLanguage: ${"$"}titleLanguage, + staffNameLanguage: ${"$"}staffNameLanguage, + activityMergeTime: ${"$"}activityMergeTime, + airingNotifications: ${"$"}airingNotifications, + displayAdultContent: ${"$"}displayAdultContent, + restrictMessagesToFollowing: ${"$"}restrictMessagesToFollowing, + scoreFormat: ${"$"}scoreFormat, + rowOrder: ${"$"}rowOrder, + ) { + id + options { + timezone + titleLanguage + staffNameLanguage + activityMergeTime + airingNotifications + displayAdultContent + restrictMessagesToFollowing + } + mediaListOptions { + scoreFormat + rowOrder + } + } + } + """.trimIndent() + + val variables = """ + { + ${timezone?.let { """"timezone":"$it"""" } ?: ""} + ${titleLanguage?.let { """"titleLanguage":"$it"""" } ?: ""} + ${staffNameLanguage?.let { """"staffNameLanguage":"$it"""" } ?: ""} + ${activityMergeTime?.let { """"activityMergeTime":$it""" } ?: ""} + ${airingNotifications?.let { """"airingNotifications":$it""" } ?: ""} + ${displayAdultContent?.let { """"displayAdultContent":$it""" } ?: ""} + ${restrictMessagesToFollowing?.let { """"restrictMessagesToFollowing":$it""" } ?: ""} + ${scoreFormat?.let { """"scoreFormat":"$it"""" } ?: ""} + ${rowOrder?.let { """"rowOrder":"$it"""" } ?: ""} + } + """.trimIndent().replace("\n", "").replace(""" """, "").replace(",}", "}") + + executeQuery(query, variables) + } + suspend fun toggleFav(anime: Boolean = true, id: Int) { - val query = - """mutation (${"$"}animeId: Int,${"$"}mangaId:Int) { ToggleFavourite(animeId:${"$"}animeId,mangaId:${"$"}mangaId){ anime { edges { id } } manga { edges { id } } } }""" + val query = """ + mutation (${"$"}animeId: Int, ${"$"}mangaId: Int) { + ToggleFavourite(animeId: ${"$"}animeId, mangaId: ${"$"}mangaId) { + anime { + edges { + id + } + } + manga { + edges { + id + } + } + } + } + """.trimIndent() val variables = if (anime) """{"animeId":"$id"}""" else """{"mangaId":"$id"}""" executeQuery(query, variables) } @@ -25,7 +108,17 @@ class AnilistMutations { FavType.STAFF -> "staffId" FavType.STUDIO -> "studioId" } - val query = """mutation{ToggleFavourite($filter:$id){anime{pageInfo{total}}}}""" + val query = """ + mutation { + ToggleFavourite($filter: $id) { + anime { + pageInfo { + total + } + } + } + } + """.trimIndent() val result = executeQuery(query) return result?.get("errors") == null && result != null } @@ -34,6 +127,51 @@ class AnilistMutations { ANIME, MANGA, CHARACTER, STAFF, STUDIO } + suspend fun deleteCustomList(name: String, type: String): Boolean { + val query = """ + mutation (${"$"}name: String, ${"$"}type: MediaType) { + DeleteCustomList(customList: ${"$"}name, type: ${"$"}type) { + deleted + } + } + """.trimIndent() + val variables = """ + { + "name": "$name", + "type": "$type" + } + """.trimIndent() + val result = executeQuery(query, variables) + return result?.get("errors") == null + } + + suspend fun updateCustomLists(animeCustomLists: List?, mangaCustomLists: List?): Boolean { + val query = """ + mutation (${"$"}animeListOptions: MediaListOptionsInput, ${"$"}mangaListOptions: MediaListOptionsInput) { + UpdateUser(animeListOptions: ${"$"}animeListOptions, mangaListOptions: ${"$"}mangaListOptions) { + mediaListOptions { + animeList { + customLists + } + mangaList { + customLists + } + } + } + } + """.trimIndent() + val variables = """ + { + ${animeCustomLists?.let { """"animeListOptions": {"customLists": ${Gson().toJson(it)}}""" } ?: ""} + ${if (animeCustomLists != null && mangaCustomLists != null) "," else ""} + ${mangaCustomLists?.let { """"mangaListOptions": {"customLists": ${Gson().toJson(it)}}""" } ?: ""} + } + """.trimIndent().replace("\n", "").replace(""" """, "").replace(",}", "}") + + val result = executeQuery(query, variables) + return result?.get("errors") == null + } + suspend fun editList( mediaID: Int, progress: Int? = null, @@ -46,14 +184,45 @@ class AnilistMutations { completedAt: FuzzyDate? = null, customList: List? = null ) { - val query = """ - mutation ( ${"$"}mediaID: Int, ${"$"}progress: Int,${"$"}private:Boolean,${"$"}repeat: Int, ${"$"}notes: String, ${"$"}customLists: [String], ${"$"}scoreRaw:Int, ${"$"}status:MediaListStatus, ${"$"}start:FuzzyDateInput${if (startedAt != null) "=" + startedAt.toVariableString() else ""}, ${"$"}completed:FuzzyDateInput${if (completedAt != null) "=" + completedAt.toVariableString() else ""} ) { - SaveMediaListEntry( mediaId: ${"$"}mediaID, progress: ${"$"}progress, repeat: ${"$"}repeat, notes: ${"$"}notes, private: ${"$"}private, scoreRaw: ${"$"}scoreRaw, status:${"$"}status, startedAt: ${"$"}start, completedAt: ${"$"}completed , customLists: ${"$"}customLists ) { - score(format:POINT_10_DECIMAL) startedAt{year month day} completedAt{year month day} + mutation ( + ${"$"}mediaID: Int, + ${"$"}progress: Int, + ${"$"}private: Boolean, + ${"$"}repeat: Int, + ${"$"}notes: String, + ${"$"}customLists: [String], + ${"$"}scoreRaw: Int, + ${"$"}status: MediaListStatus, + ${"$"}start: FuzzyDateInput${if (startedAt != null) "=" + startedAt.toVariableString() else ""}, + ${"$"}completed: FuzzyDateInput${if (completedAt != null) "=" + completedAt.toVariableString() else ""} + ) { + SaveMediaListEntry( + mediaId: ${"$"}mediaID, + progress: ${"$"}progress, + repeat: ${"$"}repeat, + notes: ${"$"}notes, + private: ${"$"}private, + scoreRaw: ${"$"}scoreRaw, + status: ${"$"}status, + startedAt: ${"$"}start, + completedAt: ${"$"}completed, + customLists: ${"$"}customLists + ) { + score(format: POINT_10_DECIMAL) + startedAt { + year + month + day + } + completedAt { + year + month + day + } } } - """.replace("\n", "").replace(""" """, "") + """.trimIndent() val variables = """{"mediaID":$mediaID ${if (private != null) ""","private":$private""" else ""} @@ -69,91 +238,170 @@ class AnilistMutations { } suspend fun deleteList(listId: Int) { - val query = "mutation(${"$"}id:Int){DeleteMediaListEntry(id:${"$"}id){deleted}}" + val query = """ + mutation(${"$"}id: Int) { + DeleteMediaListEntry(id: ${"$"}id) { + deleted + } + } + """.trimIndent() val variables = """{"id":"$listId"}""" executeQuery(query, variables) } - suspend fun rateReview(reviewId: Int, rating: String): Query.RateReviewResponse? { - val query = - "mutation{RateReview(reviewId:$reviewId,rating:$rating){id mediaId mediaType summary body(asHtml:true)rating ratingAmount userRating score private siteUrl createdAt updatedAt user{id name bannerImage avatar{medium large}}}}" + val query = """ + mutation { + RateReview(reviewId: $reviewId, rating: $rating) { + id + mediaId + mediaType + summary + body(asHtml: true) + rating + ratingAmount + userRating + score + private + siteUrl + createdAt + updatedAt + user { + id + name + bannerImage + avatar { + medium + large + } + } + } + } + """.trimIndent() return executeQuery(query) } suspend fun toggleFollow(id: Int): Query.ToggleFollow? { return executeQuery( - """mutation{ToggleFollow(userId:$id){id, isFollowing, isFollower}}""" - ) + """ + mutation { + ToggleFollow(userId: $id) { + id + isFollowing + isFollower + } + } + """.trimIndent()) } suspend fun toggleLike(id: Int, type: String): ToggleLike? { return executeQuery( - """mutation Like{ToggleLikeV2(id:$id,type:$type){__typename}}""" - ) + """ + mutation Like { + ToggleLikeV2(id: $id, type: $type) { + __typename + } + } + """.trimIndent()) } suspend fun postActivity(text: String, edit: Int? = null): String { val encodedText = text.stringSanitizer() - val query = - "mutation{SaveTextActivity(${if (edit != null) "id:$edit," else ""} text:$encodedText){siteUrl}}" + val query = """ + mutation { + SaveTextActivity(${if (edit != null) "id: $edit," else ""} text: $encodedText) { + siteUrl + } + } + """.trimIndent() val result = executeQuery(query) val errors = result?.get("errors") - return errors?.toString() - ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success") + return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success") } - suspend fun postMessage( - userId: Int, - text: String, - edit: Int? = null, - isPrivate: Boolean = false - ): String { + suspend fun postMessage(userId: Int, text: String, edit: Int? = null, isPrivate: Boolean = false): String { val encodedText = text.replace("", "").stringSanitizer() - val query = - "mutation{SaveMessageActivity(${if (edit != null) "id:$edit," else ""} recipientId:$userId,message:$encodedText,private:$isPrivate){id}}" + val query = """ + mutation { + SaveMessageActivity( + ${if (edit != null) "id: $edit," else ""} + recipientId: $userId, + message: $encodedText, + private: $isPrivate + ) { + id + } + } + """.trimIndent() val result = executeQuery(query) val errors = result?.get("errors") - return errors?.toString() - ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success") + return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success") } suspend fun postReply(activityId: Int, text: String, edit: Int? = null): String { val encodedText = text.stringSanitizer() - val query = - "mutation{SaveActivityReply(${if (edit != null) "id:$edit," else ""} activityId:$activityId,text:$encodedText){id}}" + val query = """ + mutation { + SaveActivityReply( + ${if (edit != null) "id: $edit," else ""} + activityId: $activityId, + text: $encodedText + ) { + id + } + } + """.trimIndent() val result = executeQuery(query) val errors = result?.get("errors") - return errors?.toString() - ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success") + return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success") } suspend fun postReview(summary: String, body: String, mediaId: Int, score: Int): String { val encodedSummary = summary.stringSanitizer() val encodedBody = body.stringSanitizer() - val query = - "mutation{SaveReview(mediaId:$mediaId,summary:$encodedSummary,body:$encodedBody,score:$score){siteUrl}}" + val query = """ + mutation { + SaveReview( + mediaId: $mediaId, + summary: $encodedSummary, + body: $encodedBody, + score: $score + ) { + siteUrl + } + } + """.trimIndent() val result = executeQuery(query) val errors = result?.get("errors") - return errors?.toString() - ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success") + return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success") } suspend fun deleteActivityReply(activityId: Int): Boolean { - val query = "mutation{DeleteActivityReply(id:$activityId){deleted}}" + val query = """ + mutation { + DeleteActivityReply(id: $activityId) { + deleted + } + } + """.trimIndent() val result = executeQuery(query) val errors = result?.get("errors") return errors == null } suspend fun deleteActivity(activityId: Int): Boolean { - val query = "mutation{DeleteActivity(id:$activityId){deleted}}" + val query = """ + mutation { + DeleteActivity(id: $activityId) { + deleted + } + } + """.trimIndent() val result = executeQuery(query) val errors = result?.get("errors") return errors == null } - private fun String.stringSanitizer(): String { val sb = StringBuilder() var i = 0 diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt index a834acc3..dc77be30 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt @@ -43,8 +43,8 @@ class AnilistQueries { suspend fun getUserData(): Boolean { val response: Query.Viewer? measureTimeMillis { - response = - executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}unreadNotificationCount}}""") + 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}}""") }.also { println("time : $it") } val user = response?.data?.user ?: return false @@ -61,6 +61,27 @@ class AnilistQueries { val unread = PrefManager.getVal(PrefName.UnreadCommentNotifications) Anilist.unreadNotificationCount += unread Anilist.initialized = true + + user.options?.let { + Anilist.titleLanguage = it.titleLanguage.toString() + Anilist.staffNameLanguage = it.staffNameLanguage.toString() + Anilist.airingNotifications = it.airingNotifications ?: false + Anilist.restrictMessagesToFollowing = it.restrictMessagesToFollowing ?: false + Anilist.timezone = it.timezone + Anilist.activityMergeTime = it.activityMergeTime + } + user.mediaListOptions?.let { + Anilist.scoreFormat = it.scoreFormat.toString() + Anilist.rowOrder = it.rowOrder + + it.animeList?.let { animeList -> + Anilist.animeCustomLists = animeList.customLists + } + + it.mangaList?.let { mangaList -> + Anilist.mangaCustomLists = mangaList.customLists + } + } return true } diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/User.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/User.kt index 9117c144..b887c710 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/api/User.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/User.kt @@ -74,7 +74,7 @@ data class User( @Serializable data class UserOptions( // The language the user wants to see media titles in - // @SerialName("titleLanguage") var titleLanguage: UserTitleLanguage?, + @SerialName("titleLanguage") var titleLanguage: UserTitleLanguage?, // Whether the user has enabled viewing of 18+ content @SerialName("displayAdultContent") var displayAdultContent: Boolean?, @@ -88,17 +88,17 @@ data class UserOptions( // // Notification options // // @SerialName("notificationOptions") var notificationOptions: List?, // - // // The user's timezone offset (Auth user only) - // @SerialName("timezone") var timezone: String?, + // The user's timezone offset (Auth user only) + @SerialName("timezone") var timezone: String?, // - // // Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always. - // @SerialName("activityMergeTime") var activityMergeTime: Int?, + // Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always. + @SerialName("activityMergeTime") var activityMergeTime: Int?, // - // // The language the user wants to see staff and character names in - // // @SerialName("staffNameLanguage") var staffNameLanguage: UserStaffNameLanguage?, + // The language the user wants to see staff and character names in + @SerialName("staffNameLanguage") var staffNameLanguage: UserStaffNameLanguage?, // - // // Whether the user only allow messages from users they follow - // @SerialName("restrictMessagesToFollowing") var restrictMessagesToFollowing: Boolean?, + // Whether the user only allow messages from users they follow + @SerialName("restrictMessagesToFollowing") var restrictMessagesToFollowing: Boolean?, // The list activity types the user has disabled from being created from list updates // @SerialName("disabledListActivity") var disabledListActivity: List?, @@ -119,6 +119,26 @@ data class UserStatisticTypes( @SerialName("manga") var manga: UserStatistics? ) +@Serializable +enum class UserTitleLanguage { + @SerialName("ENGLISH") + ENGLISH, + @SerialName("ROMAJI") + ROMAJI, + @SerialName("NATIVE") + NATIVE +} + +@Serializable +enum class UserStaffNameLanguage { + @SerialName("ENGLISH") + ENGLISH, + @SerialName("ROMAJI") + ROMAJI, + @SerialName("NATIVE") + NATIVE +} + @Serializable data class UserStatistics( // @@ -164,7 +184,7 @@ data class Favourites( @Serializable data class MediaListOptions( // The score format the user is using for media lists - @SerialName("scoreFormat") var scoreFormat: String?, + @SerialName("scoreFormat") var scoreFormat: ScoreFormat?, // The default order list rows should be displayed in @SerialName("rowOrder") var rowOrder: String?, @@ -176,13 +196,27 @@ data class MediaListOptions( @SerialName("mangaList") var mangaList: MediaListTypeOptions?, ) +@Serializable +enum class ScoreFormat { + @SerialName("POINT_100") + POINT_100, + @SerialName("POINT_10_DECIMAL") + POINT_10_DECIMAL, + @SerialName("POINT_10") + POINT_10, + @SerialName("POINT_5") + POINT_5, + @SerialName("POINT_3") + POINT_3, +} + @Serializable data class MediaListTypeOptions( // The order each list should be displayed in @SerialName("sectionOrder") var sectionOrder: List?, - // If the completed sections of the list should be separated by format - @SerialName("splitCompletedSectionByFormat") var splitCompletedSectionByFormat: Boolean?, + // // If the completed sections of the list should be separated by format + // @SerialName("splitCompletedSectionByFormat") var splitCompletedSectionByFormat: Boolean?, // The names of the user's custom lists @SerialName("customLists") var customLists: List?, diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt index 048ba398..d9b44e99 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt @@ -293,7 +293,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi binding.mediaTotal.visibility = View.VISIBLE binding.mediaAddToList.text = userStatus } else { - binding.mediaAddToList.setText(R.string.add) + binding.mediaAddToList.setText(R.string.add_list) } total() binding.mediaAddToList.setOnClickListener { diff --git a/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt b/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt index 52ba668b..c37e0c14 100644 --- a/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt +++ b/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt @@ -252,14 +252,14 @@ class StatsFragment : stat?.statistics?.anime?.scores?.map { convertScore( it.score, - stat.mediaListOptions.scoreFormat + stat.mediaListOptions.scoreFormat.toString() ) } ?: emptyList() } else { stat?.statistics?.manga?.scores?.map { convertScore( it.score, - stat.mediaListOptions.scoreFormat + stat.mediaListOptions.scoreFormat.toString() ) } ?: emptyList() } diff --git a/app/src/main/java/ani/dantotsu/settings/AnilistSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/AnilistSettingsActivity.kt new file mode 100644 index 00000000..684fb8db --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/AnilistSettingsActivity.kt @@ -0,0 +1,340 @@ +package ani.dantotsu.settings + +import android.os.Bundle +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.LinearLayout +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.children +import androidx.core.view.updateLayoutParams +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import ani.dantotsu.R +import ani.dantotsu.connections.anilist.Anilist +import ani.dantotsu.connections.anilist.Anilist.ScoreFormat +import ani.dantotsu.connections.anilist.Anilist.activityMergeTimeMap +import ani.dantotsu.connections.anilist.Anilist.rowOrderMap +import ani.dantotsu.connections.anilist.Anilist.staffNameLang +import ani.dantotsu.connections.anilist.Anilist.titleLang +import ani.dantotsu.connections.anilist.AnilistMutations +import ani.dantotsu.databinding.ActivitySettingsAnilistBinding +import ani.dantotsu.initActivity +import ani.dantotsu.navBarHeight +import ani.dantotsu.restartApp +import ani.dantotsu.statusBarHeight +import ani.dantotsu.themes.ThemeManager +import ani.dantotsu.toast +import ani.dantotsu.util.customAlertDialog +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import kotlinx.coroutines.launch + +class AnilistSettingsActivity : AppCompatActivity() { + private lateinit var binding: ActivitySettingsAnilistBinding + private lateinit var anilistMutations: AnilistMutations + + enum class FormatLang { + ENGLISH, + ROMAJI, + NATIVE + } + + enum class FormatScore { + POINT_100, + POINT_10_DECIMAL, + POINT_10, + POINT_5, + POINT_3, + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ThemeManager(this).applyTheme() + initActivity(this) + val context = this + binding = ActivitySettingsAnilistBinding.inflate(layoutInflater) + setContentView(binding.root) + + anilistMutations = AnilistMutations() + + binding.apply { + settingsAnilistLayout.updateLayoutParams { + topMargin = statusBarHeight + bottomMargin = navBarHeight + } + binding.anilistSettingsBack.setOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + + val currentTitleLang = Anilist.titleLanguage + val titleFormat = FormatLang.entries.firstOrNull { it.name == currentTitleLang } ?: FormatLang.ENGLISH + + settingsAnilistTitleLanguage.setText(titleLang[titleFormat.ordinal]) + settingsAnilistTitleLanguage.setAdapter( + ArrayAdapter(context, R.layout.item_dropdown, titleLang) + ) + settingsAnilistTitleLanguage.setOnItemClickListener { _, _, i, _ -> + val selectedLanguage = when (i) { + 0 -> "ENGLISH" + 1 -> "ROMAJI" + 2 -> "NATIVE" + else -> "ENGLISH" + } + lifecycleScope.launch { + anilistMutations.updateSettings(titleLanguage = selectedLanguage) + Anilist.titleLanguage = selectedLanguage + restartApp() + } + settingsAnilistTitleLanguage.clearFocus() + } + + val currentStaffNameLang = Anilist.staffNameLanguage + val staffNameFormat = FormatLang.entries.firstOrNull { it.name == currentStaffNameLang } ?: FormatLang.ENGLISH + + settingsAnilistStaffLanguage.setText(staffNameLang[staffNameFormat.ordinal]) + settingsAnilistStaffLanguage.setAdapter( + ArrayAdapter(context, R.layout.item_dropdown, staffNameLang) + ) + settingsAnilistStaffLanguage.setOnItemClickListener { _, _, i, _ -> + val selectedLanguage = when (i) { + 0 -> "ENGLISH" + 1 -> "ROMAJI" + 2 -> "NATIVE" + else -> "ENGLISH" + } + lifecycleScope.launch { + anilistMutations.updateSettings(staffNameLanguage = selectedLanguage) + Anilist.staffNameLanguage = selectedLanguage + restartApp() + } + settingsAnilistStaffLanguage.clearFocus() + } + + val currentMergeTimeDisplay = activityMergeTimeMap.entries.firstOrNull { it.value == Anilist.activityMergeTime }?.key + ?: "${Anilist.activityMergeTime} mins" + settingsAnilistActivityMergeTime.setText(currentMergeTimeDisplay) + settingsAnilistActivityMergeTime.setAdapter( + ArrayAdapter(context, R.layout.item_dropdown, activityMergeTimeMap.keys.toList()) + ) + settingsAnilistActivityMergeTime.setOnItemClickListener { _, _, i, _ -> + val selectedDisplayTime = activityMergeTimeMap.keys.toList()[i] + val selectedApiTime = activityMergeTimeMap[selectedDisplayTime] ?: 0 + lifecycleScope.launch { + anilistMutations.updateSettings(activityMergeTime = selectedApiTime) + Anilist.activityMergeTime = selectedApiTime + restartApp() + } + settingsAnilistActivityMergeTime.clearFocus() + } + + val currentScoreFormat = Anilist.scoreFormat + val scoreFormat = FormatScore.entries.firstOrNull{ it.name == currentScoreFormat } ?: FormatScore.POINT_100 + settingsAnilistScoreFormat.setText(ScoreFormat[scoreFormat.ordinal]) + settingsAnilistScoreFormat.setAdapter( + ArrayAdapter(context, R.layout.item_dropdown, ScoreFormat) + ) + settingsAnilistScoreFormat.setOnItemClickListener { _, _, i, _ -> + val selectedFormat = when (i) { + 0 -> "POINT_100" + 1 -> "POINT_10_DECIMAL" + 2 -> "POINT_10" + 3 -> "POINT_5" + 4 -> "POINT_3" + else -> "POINT_100" + } + lifecycleScope.launch { + anilistMutations.updateSettings(scoreFormat = selectedFormat) + Anilist.scoreFormat = selectedFormat + restartApp() + } + settingsAnilistScoreFormat.clearFocus() + } + + val currentRowOrder = rowOrderMap.entries.firstOrNull { it.value == Anilist.rowOrder }?.key ?: "Score" + settingsAnilistRowOrder.setText(currentRowOrder) + settingsAnilistRowOrder.setAdapter( + ArrayAdapter(context, R.layout.item_dropdown, rowOrderMap.keys.toList()) + ) + settingsAnilistRowOrder.setOnItemClickListener { _, _, i, _ -> + val selectedDisplayOrder = rowOrderMap.keys.toList()[i] + val selectedApiOrder = rowOrderMap[selectedDisplayOrder] ?: "score" + lifecycleScope.launch { + anilistMutations.updateSettings(rowOrder = selectedApiOrder) + Anilist.rowOrder = selectedApiOrder + restartApp() + } + settingsAnilistRowOrder.clearFocus() + } + + val containers = listOf(binding.animeCustomListsContainer, binding.mangaCustomListsContainer) + val customLists = listOf(Anilist.animeCustomLists, Anilist.mangaCustomLists) + val buttons = listOf(binding.addAnimeListButton, binding.addMangaListButton) + + containers.forEachIndexed { index, container -> + customLists[index]?.forEach { listName -> + addCustomListItem(listName, container, index == 0) + } + } + + buttons.forEachIndexed { index, button -> + button.setOnClickListener { + addCustomListItem("", containers[index], index == 0) + } + } + + binding.SettingsAnilistCustomListSave.setOnClickListener { + saveCustomLists() + } + + val currentTimezone = Anilist.timezone?.let { Anilist.getDisplayTimezone(it) } ?: "(GMT+00:00) London" + settingsAnilistTimezone.setText(currentTimezone) + settingsAnilistTimezone.setAdapter( + ArrayAdapter(context, R.layout.item_dropdown, Anilist.timeZone) + ) + settingsAnilistTimezone.setOnItemClickListener { _, _, i, _ -> + val selectedTimezone = Anilist.timeZone[i] + val apiTimezone = Anilist.getApiTimezone(selectedTimezone) + lifecycleScope.launch { + anilistMutations.updateSettings(timezone = apiTimezone) + Anilist.timezone = apiTimezone + restartApp() + } + settingsAnilistTimezone.clearFocus() + } + + val displayAdultContent = Anilist.adult + val airingNotifications = Anilist.airingNotifications + + binding.settingsRecyclerView1.adapter = SettingsAdapter( + arrayListOf( + Settings( + type = 2, + name = getString(R.string.airing_notifications), + desc = getString(R.string.airing_notifications_desc), + icon = R.drawable.ic_round_notifications_active_24, + isChecked = airingNotifications, + switch = { isChecked, _ -> + lifecycleScope.launch { + anilistMutations.updateSettings(airingNotifications = isChecked) + Anilist.airingNotifications = isChecked + restartApp() + } + } + ), + Settings( + type = 2, + name = getString(R.string.display_adult_content), + desc = getString(R.string.display_adult_content_desc), + icon = R.drawable.ic_round_nsfw_24, + isChecked = displayAdultContent, + switch = { isChecked, _ -> + lifecycleScope.launch { + anilistMutations.updateSettings(displayAdultContent = isChecked) + Anilist.adult = isChecked + restartApp() + } + } + ), + ) + ) + binding.settingsRecyclerView1.layoutManager = + LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + + } + + binding.settingsRecyclerView2.adapter = SettingsAdapter( + arrayListOf( + Settings( + type = 2, + name = getString(R.string.restrict_messages), + desc = getString(R.string.restrict_messages_desc), + icon = R.drawable.ic_round_lock_open_24, + isChecked = Anilist.restrictMessagesToFollowing, + switch = { isChecked, _ -> + lifecycleScope.launch { + anilistMutations.updateSettings(restrictMessagesToFollowing = isChecked) + Anilist.restrictMessagesToFollowing = isChecked + restartApp() + } + } + ), + ) + ) + binding.settingsRecyclerView2.layoutManager = + LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + + } + + private fun addCustomListItem(listName: String, container: LinearLayout, isAnime: Boolean) { + val customListItemView = layoutInflater.inflate(R.layout.item_custom_list, container, false) + val textInputLayout = customListItemView.findViewById(R.id.customListItem) + val editText = textInputLayout.editText as? TextInputEditText + editText?.setText(listName) + textInputLayout.setEndIconOnClickListener { + val name = editText?.text.toString() + if (name.isNotEmpty()) { + val listExists = if (isAnime) { + Anilist.animeCustomLists?.contains(name) ?: false + } else { + Anilist.mangaCustomLists?.contains(name) ?: false + } + + if (listExists) { + customAlertDialog().apply { + setTitle(getString(R.string.delete_custom_list)) + setMessage(getString(R.string.delete_custom_list_confirm, name)) + setPosButton(getString(R.string.delete)) { + deleteCustomList(name, isAnime) + container.removeView(customListItemView) + } + setNegButton(getString(R.string.cancel)) + }.show() + } else { + container.removeView(customListItemView) + } + } else { + container.removeView(customListItemView) + } + } + container.addView(customListItemView) + } + + private fun deleteCustomList(name: String, isAnime: Boolean) { + lifecycleScope.launch { + val type = if (isAnime) "ANIME" else "MANGA" + val success = anilistMutations.deleteCustomList(name, type) + if (success) { + if (isAnime) { + Anilist.animeCustomLists = Anilist.animeCustomLists?.filter { it != name } + } else { + Anilist.mangaCustomLists = Anilist.mangaCustomLists?.filter { it != name } + } + toast("Custom list deleted") + } else { + toast("Failed to delete custom list") + } + } + } + + private fun saveCustomLists() { + val animeCustomLists = binding.animeCustomListsContainer.children + .mapNotNull { (it.findViewById(R.id.customListItem).editText as? TextInputEditText)?.text?.toString() } + .filter { it.isNotEmpty() } + .toList() + val mangaCustomLists = binding.mangaCustomListsContainer.children + .mapNotNull { (it.findViewById(R.id.customListItem).editText as? TextInputEditText)?.text?.toString() } + .filter { it.isNotEmpty() } + .toList() + + lifecycleScope.launch { + val success = anilistMutations.updateCustomLists(animeCustomLists, mangaCustomLists) + if (success) { + Anilist.animeCustomLists = animeCustomLists + Anilist.mangaCustomLists = mangaCustomLists + toast("Custom lists saved") + } else { + toast("Failed to save custom lists") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt index 46d42eb9..dd11df3a 100644 --- a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt @@ -21,7 +21,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.R import ani.dantotsu.copyToClipboard -import ani.dantotsu.currContext import ani.dantotsu.databinding.ActivityExtensionsBinding import ani.dantotsu.databinding.DialogRepositoriesBinding import ani.dantotsu.databinding.ItemRepositoryBinding @@ -321,7 +320,7 @@ class ExtensionsActivity : AppCompatActivity() { customAlertDialog().apply { setTitle(R.string.edit_repositories) setCustomView(dialogView.root) - setPosButton(R.string.add) { + setPosButton(R.string.add_list) { if (!dialogView.repositoryTextBox.text.isNullOrBlank()) { processUserInput(dialogView.repositoryTextBox.text.toString(), type) } diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsAccountActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsAccountActivity.kt index 7bebb68f..204feb97 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsAccountActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsAccountActivity.kt @@ -1,5 +1,6 @@ package ani.dantotsu.settings +import android.content.Intent import android.os.Bundle import android.view.HapticFeedbackConstants import android.view.View @@ -9,6 +10,8 @@ import android.widget.TextView import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.view.updateLayoutParams +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.discord.Discord @@ -26,6 +29,7 @@ import ani.dantotsu.statusBarHeight import ani.dantotsu.themes.ThemeManager import io.noties.markwon.Markwon import io.noties.markwon.SoftBreakAddsNewLinePlugin +import kotlinx.coroutines.launch class SettingsAccountActivity : AppCompatActivity() { private lateinit var binding: ActivitySettingsAccountsBinding @@ -111,6 +115,7 @@ class SettingsAccountActivity : AppCompatActivity() { } else { settingsAnilistAvatar.setImageResource(R.drawable.ic_round_person_24) settingsAnilistUsername.visibility = View.GONE + settingsRecyclerView.visibility = View.GONE settingsAnilistLogin.setText(R.string.login) settingsAnilistLogin.setOnClickListener { Anilist.loginIntent(context) @@ -142,7 +147,7 @@ class SettingsAccountActivity : AppCompatActivity() { reload() } - settingsImageSwitcher.visibility = View.VISIBLE + settingsPresenceSwitcher.visibility = View.VISIBLE var initialStatus = when (PrefManager.getVal(PrefName.DiscordStatus)) { "online" -> R.drawable.discord_status_online "idle" -> R.drawable.discord_status_idle @@ -150,11 +155,11 @@ class SettingsAccountActivity : AppCompatActivity() { "invisible" -> R.drawable.discord_status_invisible else -> R.drawable.discord_status_online } - settingsImageSwitcher.setImageResource(initialStatus) + settingsPresenceSwitcher.setImageResource(initialStatus) val zoomInAnimation = AnimationUtils.loadAnimation(context, R.anim.bounce_zoom) - settingsImageSwitcher.setOnClickListener { + settingsPresenceSwitcher.setOnClickListener { var status = "online" initialStatus = when (initialStatus) { R.drawable.discord_status_online -> { @@ -181,16 +186,16 @@ class SettingsAccountActivity : AppCompatActivity() { } PrefManager.setVal(PrefName.DiscordStatus, status) - settingsImageSwitcher.setImageResource(initialStatus) - settingsImageSwitcher.startAnimation(zoomInAnimation) + settingsPresenceSwitcher.setImageResource(initialStatus) + settingsPresenceSwitcher.startAnimation(zoomInAnimation) } - settingsImageSwitcher.setOnLongClickListener { + settingsPresenceSwitcher.setOnLongClickListener { it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) DiscordDialogFragment().show(supportFragmentManager, "dialog") true } } else { - settingsImageSwitcher.visibility = View.GONE + settingsPresenceSwitcher.visibility = View.GONE settingsDiscordAvatar.setImageResource(R.drawable.ic_round_person_24) settingsDiscordUsername.visibility = View.GONE settingsDiscordLogin.setText(R.string.login) @@ -202,6 +207,25 @@ class SettingsAccountActivity : AppCompatActivity() { } reload() } - } + binding.settingsRecyclerView.adapter = SettingsAdapter( + arrayListOf( + Settings( + type = 1, + name = getString(R.string.anilist_settings), + desc = getString(R.string.alsettings_desc), + icon = R.drawable.ic_anilist, + onClick = { + lifecycleScope.launch { + Anilist.query.getUserData() + startActivity(Intent(context, AnilistSettingsActivity::class.java)) + } + }, + isActivity = true + ), + ) + ) + binding.settingsRecyclerView.layoutManager = + LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + } } diff --git a/app/src/main/res/drawable/ic_round_lock_24.xml b/app/src/main/res/drawable/ic_round_lock_24.xml index 68cb9c1f..87281bf0 100644 --- a/app/src/main/res/drawable/ic_round_lock_24.xml +++ b/app/src/main/res/drawable/ic_round_lock_24.xml @@ -1,6 +1,7 @@ + + @@ -281,27 +291,13 @@ - - - + android:layout_height="match_parent" + android:nestedScrollingEnabled="false" + android:requiresFadingEdge="vertical" + tools:itemCount="1" + tools:listitem="@layout/item_settings" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings_anilist.xml b/app/src/main/res/layout/activity_settings_anilist.xml new file mode 100644 index 00000000..aa7bdd9e --- /dev/null +++ b/app/src/main/res/layout/activity_settings_anilist.xml @@ -0,0 +1,419 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +