feat(settings): fully fledged AniList settings (#453)

This commit is contained in:
ibo 2024-07-08 20:13:30 +02:00 committed by GitHub
parent 7366aa1bf2
commit 04b9b9e7ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1345 additions and 99 deletions

View file

@ -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<String>? = null
var mangaCustomLists: List<String>? = 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<String, Int> {
var newSeason = if (next) currentSeason + 1 else currentSeason - 1
var newYear = currentYear

View file

@ -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<JsonObject>(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<JsonObject>(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<JsonObject>(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<JsonObject>(query, variables)
return result?.get("errors") == null
}
suspend fun updateCustomLists(animeCustomLists: List<String>?, mangaCustomLists: List<String>?): 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<JsonObject>(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<String>? = 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<JsonObject>(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.RateReviewResponse>(query)
}
suspend fun toggleFollow(id: Int): Query.ToggleFollow? {
return executeQuery<Query.ToggleFollow>(
"""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<ToggleLike>(
"""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<JsonObject>(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<JsonObject>(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<JsonObject>(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<JsonObject>(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<JsonObject>(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<JsonObject>(query)
val errors = result?.get("errors")
return errors == null
}
private fun String.stringSanitizer(): String {
val sb = StringBuilder()
var i = 0

View file

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

View file

@ -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<NotificationOption>?,
//
// // 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<ListActivityOption>?,
@ -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<String>?,
// 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<String>?,

View file

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

View file

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

View file

@ -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<ViewGroup.MarginLayoutParams> {
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<TextInputLayout>(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<TextInputLayout>(R.id.customListItem).editText as? TextInputEditText)?.text?.toString() }
.filter { it.isNotEmpty() }
.toList()
val mangaCustomLists = binding.mangaCustomListsContainer.children
.mapNotNull { (it.findViewById<TextInputLayout>(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")
}
}
}
}

View file

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

View file

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