diff --git a/app/build.gradle b/app/build.gradle index db475477..4240077a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -44,7 +44,7 @@ android { applicationIdSuffix ".beta" // keep as beta by popular request versionNameSuffix "-alpha02" manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_alpha", icon_placeholder_round: "@mipmap/ic_launcher_alpha_round"] - debuggable System.getenv("CI") == null + debuggable true isDefault true } debug { 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 a10f327c..289d8c90 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt @@ -608,12 +608,11 @@ class AnilistQueries { private suspend fun bannerImage(type: String): String? { - //var image = loadData("banner_$type") - val image: BannerImage? = BannerImage( - PrefManager.getCustomVal("banner_${type}_url", null), + val image = BannerImage( + PrefManager.getCustomVal("banner_${type}_url", ""), PrefManager.getCustomVal("banner_${type}_time", 0L) ) - if (image == null || image.checkTime()) { + if (image.url.isNullOrEmpty() || image.checkTime()) { val response = executeQuery("""{ MediaListCollection(userId: ${Anilist.userid}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } } } """) val random = response?.data?.mediaListCollection?.lists?.mapNotNull { diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt index f309ece8..bb17bf92 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt @@ -299,12 +299,12 @@ class Query { data class StatisticsResponse( @SerialName("data") val data: Data - ) { + ): java.io.Serializable { @Serializable data class Data( @SerialName("User") val user: StatisticsUser? - ) + ): java.io.Serializable } @Serializable diff --git a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt index 5ac83c89..0b2f831d 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt @@ -1260,7 +1260,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL subtitle = intent.getSerialized("subtitle") ?: when (val subLang: String? = - PrefManager.getCustomVal("subLang_${media.id}", null as String?)) { + PrefManager.getNullableCustomVal("subLang_${media.id}", null, String::class.java)) { null -> { when (episode.selectedSubtitle) { null -> null diff --git a/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt index 3fae8c79..8b172fe1 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt @@ -68,7 +68,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() { binding.subtitleTitle.setText(R.string.none) model.getMedia().observe(viewLifecycleOwner) { media -> val mediaID: Int = media.id - val selSubs = PrefManager.getCustomVal("subLang_${mediaID}", null) + val selSubs = PrefManager.getNullableCustomVal("subLang_${mediaID}", null, String::class.java) if (episode.selectedSubtitle != null && selSubs != "None") { binding.root.setCardBackgroundColor(TRANSPARENT) } @@ -108,7 +108,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() { model.getMedia().observe(viewLifecycleOwner) { media -> val mediaID: Int = media.id val selSubs: String? = - PrefManager.getCustomVal("subLang_${mediaID}", null) + PrefManager.getNullableCustomVal("subLang_${mediaID}", null, String::class.java) if (episode.selectedSubtitle != position - 1 && selSubs != subtitles[position - 1].language) { binding.root.setCardBackgroundColor(TRANSPARENT) } diff --git a/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt b/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt index b5d2f883..974c41bf 100644 --- a/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt +++ b/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt @@ -15,6 +15,8 @@ import ani.dantotsu.loadImage import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.profile.ProfileActivity import ani.dantotsu.snackString +import ani.dantotsu.util.ColorEditor.Companion.adjustColorForContrast +import ani.dantotsu.util.ColorEditor.Companion.getContrastRatio import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.Section import com.xwray.groupie.viewbinding.BindableItem @@ -300,25 +302,6 @@ class CommentItem(val comment: Comment, } } - private fun getLuminance(color: Int): Double { - val r = Color.red(color) / 255.0 - val g = Color.green(color) / 255.0 - val b = Color.blue(color) / 255.0 - - val rL = if (r <= 0.03928) r / 12.92 else ((r + 0.055) / 1.055).pow(2.4) - val gL = if (g <= 0.03928) g / 12.92 else ((g + 0.055) / 1.055).pow(2.4) - val bL = if (b <= 0.03928) b / 12.92 else ((b + 0.055) / 1.055).pow(2.4) - - return 0.2126 * rL + 0.7152 * gL + 0.0722 * bL - } - - private fun getContrastRatio(color1: Int, color2: Int): Double { - val l1 = getLuminance(color1) - val l2 = getLuminance(color2) - - return if (l1 > l2) (l1 + 0.05) / (l2 + 0.05) else (l2 + 0.05) / (l1 + 0.05) - } - private fun getAvatarColor(voteCount: Int, backgroundColor: Int): Pair { val level = if (voteCount < 0) 0 else sqrt(abs(voteCount.toDouble()) / 0.8).toInt() val colorString = if (level > usernameColors.size - 1) usernameColors[usernameColors.size - 1] else usernameColors[level] @@ -331,37 +314,6 @@ class CommentItem(val comment: Comment, return Pair(color, level) } - private fun adjustColorForContrast(originalColor: Int, backgroundColor: Int): Int { - var adjustedColor = originalColor - var contrastRatio = getContrastRatio(adjustedColor, backgroundColor) - val isBackgroundDark = getLuminance(backgroundColor) < 0.5 - - while (contrastRatio < 4.5) { - // Adjust brightness by modifying the RGB values - val r = Color.red(adjustedColor) - val g = Color.green(adjustedColor) - val b = Color.blue(adjustedColor) - - // Calculate the amount to adjust - val adjustment = if (isBackgroundDark) 10 else -10 - - // Adjust the color - val newR = (r + adjustment).coerceIn(0, 255) - val newG = (g + adjustment).coerceIn(0, 255) - val newB = (b + adjustment).coerceIn(0, 255) - - adjustedColor = Color.rgb(newR, newG, newB) - contrastRatio = getContrastRatio(adjustedColor, backgroundColor) - - // Break the loop if the color adjustment does not change (to avoid infinite loop) - if (newR == r && newG == g && newB == b) { - break - } - } - return adjustedColor - } - - /** * Builds the dialog for yes/no confirmation * no doesn't do anything, yes calls the callback diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt index 1c73c6f0..5c11ae35 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt @@ -440,7 +440,7 @@ class MangaReadAdapter( if (media.manga?.chapters != null) { val chapters = media.manga.chapters!!.keys.toTypedArray() val anilistEp = (media.userProgress ?: 0).plus(1) - val appEp = PrefManager.getCustomVal("${media.id}_current_chp", null) + val appEp = PrefManager.getNullableCustomVal("${media.id}_current_chp", null, String::class.java) ?.toIntOrNull() ?: 1 var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString() val filteredChapters = chapters.filter { chapterKey -> diff --git a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt index b77cdc77..13712c21 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt @@ -291,7 +291,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener { applySettings() } - val cfi = PrefManager.getCustomVal("${sanitizedBookId}_progress", null as String?) + val cfi = PrefManager.getNullableCustomVal("${sanitizedBookId}_progress", null, String::class.java) cfi?.let { binding.bookReader.goto(it) } binding.progress.visibility = View.GONE diff --git a/app/src/main/java/ani/dantotsu/profile/ChartBuilder.kt b/app/src/main/java/ani/dantotsu/profile/ChartBuilder.kt index 43475c07..f9a81ee4 100644 --- a/app/src/main/java/ani/dantotsu/profile/ChartBuilder.kt +++ b/app/src/main/java/ani/dantotsu/profile/ChartBuilder.kt @@ -3,6 +3,7 @@ package ani.dantotsu.profile import android.content.Context import android.graphics.Color import android.util.TypedValue +import ani.dantotsu.util.ColorEditor import com.github.aachartmodel.aainfographics.aachartcreator.AAChartModel import com.github.aachartmodel.aainfographics.aachartcreator.AAChartStackingType import com.github.aachartmodel.aainfographics.aachartcreator.AAChartType @@ -17,6 +18,7 @@ import com.github.aachartmodel.aainfographics.aaoptionsmodel.AAScrollablePlotAre import com.github.aachartmodel.aainfographics.aaoptionsmodel.AAStyle import com.github.aachartmodel.aainfographics.aaoptionsmodel.AAYAxis import com.github.aachartmodel.aainfographics.aatools.AAColor +import com.github.aachartmodel.aainfographics.aatools.AAGradientColor class ChartBuilder { companion object { @@ -25,25 +27,30 @@ class ChartBuilder { } enum class StatType { - COUNT, TIME, MEAN_SCORE + COUNT, TIME, AVG_SCORE } enum class MediaType { ANIME, MANGA } + data class ChartPacket( + val username: String, + val names: List, + val statData: List + ) + fun buildChart( context: Context, - chartType: ChartType, - aaChartType: AAChartType, + passedChartType: ChartType, + passedAaChartType: AAChartType, statType: StatType, mediaType: MediaType, - names: List, - statData: List, + chartPackets: List, xAxisName: String = "X Axis", xAxisTickInterval: Int? = null, polar: Boolean = false, - categories: List? = null, + passedCategories: List? = null, scrollPos: Float? = null, ): AAOptions { val typedValue = TypedValue() @@ -53,7 +60,18 @@ class ChartBuilder { true ) val primaryColor = typedValue.data - val palette = generateColorPalette(primaryColor, names.size) + var chartType = passedChartType + var aaChartType = passedAaChartType + var categories = passedCategories + if (chartType == ChartType.OneDimensional && chartPackets.size != 1) { + //need to convert to 2D + chartType = ChartType.TwoDimensional + aaChartType = AAChartType.Column + categories = chartPackets[0].names.map { it.toString() } + } + + val namesMax = chartPackets.maxOf { it.names.size } + val palette = ColorEditor.generateColorPalette(primaryColor, namesMax) val aaChartModel = when (chartType) { ChartType.OneDimensional -> { val chart = AAChartModel() @@ -61,14 +79,26 @@ class ChartBuilder { .subtitle(getTypeName(statType, mediaType)) .zoomType(AAChartZoomType.None) .dataLabelsEnabled(true) - .series( - get1DElements( - names, - statData, - palette, - primaryColor - ) - ) + val elements: MutableList = mutableListOf() + chartPackets.forEachIndexed { index, chartPacket -> + val element = AASeriesElement() + .name(chartPacket.username) + .data( + get1DElements( + chartPacket.names, + chartPacket.statData, + palette + ) + ) + if (index == 0) { + element.color(primaryColor) + } else { + element.color(ColorEditor.oppositeColor(primaryColor)) + } + elements.add(element) + + } + chart.series(elements.toTypedArray()) xAxisTickInterval?.let { chart.xAxisTickInterval(it) } categories?.let { chart.categories(it.toTypedArray()) } chart @@ -83,9 +113,31 @@ class ChartBuilder { .zoomType(AAChartZoomType.None) .dataLabelsEnabled(false) .yAxisTitle(getTypeName(statType, mediaType)) - .stacking(AAChartStackingType.Normal) - .series(get2DElements(names, statData, primaryColor)) - .colorsTheme(hexColorsArray) + if (chartPackets.size == 1) { + chart.colorsTheme(hexColorsArray) + } + + val elements: MutableList = mutableListOf() + chartPackets.forEachIndexed { index, chartPacket -> + val element = get2DElements( + chartPacket.names, + chartPacket.statData, + chartPackets.size == 1 + ) + element.name(chartPacket.username) + + if (index == 0) { + element.color(AAColor.rgbaColor(Color.red(primaryColor), Color.green(primaryColor), Color.blue(primaryColor), 0.9f)) + + } else { + element.color(AAColor.rgbaColor(Color.red(ColorEditor.oppositeColor(primaryColor)), Color.green(ColorEditor.oppositeColor(primaryColor)), Color.blue(ColorEditor.oppositeColor(primaryColor)), 0.9f)) + } + if (chartPackets.size == 1) { + element.fillColor(AAColor.rgbaColor(Color.red(primaryColor), Color.green(primaryColor), Color.blue(primaryColor), 0.9f)) + } + elements.add(element) + } + chart.series(elements.toTypedArray()) xAxisTickInterval?.let { chart.xAxisTickInterval(it) } categories?.let { chart.categories(it.toTypedArray()) } @@ -101,24 +153,32 @@ class ChartBuilder { getToolTipFunction( chartType, xAxisName, - getTypeName(statType, mediaType) + getTypeName(statType, mediaType), + chartPackets.size ) ) + if (chartPackets.size > 1) { + useHTML(true) + } } aaOptions.legend?.apply { enabled(true) - .labelFormat = "{name}: {y}" + .labelFormat = "{name}" } - aaOptions.plotOptions?.series?.connectNulls(true) + aaOptions.plotOptions?.series?.connectNulls(false) + aaOptions.plotOptions?.series?.stacking(AAChartStackingType.False) aaOptions.chart?.panning = true scrollPos?.let { aaOptions.chart?.scrollablePlotArea(AAScrollablePlotArea().scrollPositionX(scrollPos)) - aaOptions.chart?.scrollablePlotArea?.minWidth((context.resources.displayMetrics.widthPixels.toFloat() / context.resources.displayMetrics.density) * (names.size.toFloat() / 18.0f)) + aaOptions.chart?.scrollablePlotArea?.minWidth((context.resources.displayMetrics.widthPixels.toFloat() / context.resources.displayMetrics.density) * (namesMax.toFloat() / 18.0f)) } - val min = ((statData.minOfOrNull { it.toDouble() } ?: 0.0) - 1.0).coerceAtLeast(0.0) - val max = statData.maxOfOrNull { it.toDouble() } ?: 0.0 - val aaYaxis = AAYAxis().min(min).max(max) + val allStatData = chartPackets.flatMap { it.statData } + val min = (allStatData.minOfOrNull { it.toDouble() } ?: 0.0) - 1.0 + val coercedMin = min.coerceAtLeast(0.0) + val max = allStatData.maxOfOrNull { it.toDouble() } ?: 0.0 + + val aaYaxis = AAYAxis().min(coercedMin).max(max) val tickInterval = when (max) { in 0.0..10.0 -> 1.0 in 10.0..30.0 -> 5.0 @@ -138,34 +198,25 @@ class ChartBuilder { private fun get2DElements( names: List, statData: List, - primaryColor: Int - ): Array { + colorByPoint: Boolean + ): AASeriesElement { val statValues = mutableListOf>() for (i in statData.indices) { statValues.add(arrayOf(names[i], statData[i], statData[i])) } - return arrayOf( - AASeriesElement().name("Score") + return AASeriesElement() .data(statValues.toTypedArray()) .dataLabels( AADataLabels() .enabled(false) ) - .colorByPoint(true) - .fillColor(AAColor.rgbaColor( - Color.red(primaryColor), - Color.green(primaryColor), - Color.blue(primaryColor), - 0.9f - )) - ) + .colorByPoint(colorByPoint) } private fun get1DElements( names: List, statData: List, - colors: List, - primaryColor: Int + colors: List ): Array { val statDataElements = mutableListOf() for (i in statData.indices) { @@ -193,44 +244,17 @@ class ChartBuilder { } statDataElements.add(element) } - return arrayOf( - AASeriesElement().name("Score").color(primaryColor) - .data(statDataElements.toTypedArray()) - ) + return statDataElements.toTypedArray() } private fun getTypeName(statType: StatType, mediaType: MediaType): String { return when (statType) { StatType.COUNT -> "Count" StatType.TIME -> if (mediaType == MediaType.ANIME) "Hours Watched" else "Chapters Read" - StatType.MEAN_SCORE -> "Mean Score" + StatType.AVG_SCORE -> "Mean Score" } } - private fun generateColorPalette( - baseColor: Int, - size: Int, - hueDelta: Float = 8f, - saturationDelta: Float = 2.02f, - valueDelta: Float = 2.02f - ): List { - val palette = mutableListOf() - val hsv = FloatArray(3) - Color.colorToHSV(baseColor, hsv) - - for (i in 0 until size) { - val newHue = - (hsv[0] + hueDelta * i) % 360 // Ensure hue stays within the 0-360 range - val newSaturation = (hsv[1] + saturationDelta * i).coerceIn(0f, 1f) - val newValue = (hsv[2] + valueDelta * i).coerceIn(0f, 1f) - - val newHsv = floatArrayOf(newHue, newSaturation, newValue) - palette.add(Color.HSVToColor(newHsv)) - } - - return palette - } - private fun setColors(aaOptions: AAOptions, context: Context, primaryColor: Int) { val backgroundColor = TypedValue() context.theme.resolveAttribute( @@ -265,15 +289,15 @@ class ChartBuilder { aaOptions.chart?.backgroundColor(backgroundStyle.color) aaOptions.tooltip?.backgroundColor( AAColor.rgbaColor( - Color.red(primaryColor), - Color.green(primaryColor), - Color.blue(primaryColor), + Color.red(backgroundColor.data), + Color.green(backgroundColor.data), + Color.blue(backgroundColor.data), 0.9f ) ) aaOptions.title?.style(onBackgroundStyle) aaOptions.subtitle?.style(onBackgroundStyle) - aaOptions.tooltip?.style(backgroundStyle) + aaOptions.tooltip?.style(onBackgroundStyle) aaOptions.credits?.style(onBackgroundStyle) aaOptions.xAxis?.labels?.style(onBackgroundStyle) aaOptions.yAxis?.labels?.style(onBackgroundStyle) @@ -287,7 +311,8 @@ class ChartBuilder { private fun getToolTipFunction( chartType: ChartType, type: String, - typeName: String + typeName: String, + chartSize: Int ): String { return when (chartType) { ChartType.OneDimensional -> { @@ -305,7 +330,8 @@ class ChartBuilder { } ChartType.TwoDimensional -> { - """ + if (chartSize == 1) { + """ function () { return '$type: ' + this.x + @@ -314,8 +340,34 @@ class ChartBuilder { this.y } """.trimIndent() + } else { + """ +function() { + let wholeContentStr = '◉${type}: ' + this.x + '
'; + if (this.points) { + let length = this.points.length; + for (let i = 0; i < length; i++) { + let thisPoint = this.points[i]; + let yValue = thisPoint.y; + if (yValue != 0) { + let spanStyleStartStr = '◉ '; + let spanStyleEndStr = '
'; + wholeContentStr += spanStyleStartStr + thisPoint.series.name + ': ' + yValue + spanStyleEndStr; + + } + } + } else { + let spanStyleStartStr = '◉ '; + let spanStyleEndStr = '
'; + wholeContentStr += spanStyleStartStr + this.point.series.name + ': ' + this.point.y + spanStyleEndStr; + } + return wholeContentStr; +} + """.trimIndent() + } } } } + } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt b/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt index d42b85f0..687a18d5 100644 --- a/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt +++ b/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt @@ -15,19 +15,19 @@ import ani.dantotsu.databinding.FragmentStatisticsBinding import ani.dantotsu.profile.ChartBuilder.Companion.ChartType import ani.dantotsu.profile.ChartBuilder.Companion.StatType import ani.dantotsu.profile.ChartBuilder.Companion.MediaType +import ani.dantotsu.profile.ChartBuilder.Companion.ChartPacket import com.github.aachartmodel.aainfographics.aachartcreator.AAChartType -import com.github.aachartmodel.aainfographics.aaoptionsmodel.AAYAxis import com.xwray.groupie.GroupieAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.Locale -class StatsFragment() : +class StatsFragment : Fragment() { private lateinit var binding: FragmentStatisticsBinding private var adapter: GroupieAdapter = GroupieAdapter() - private var stats: Query.StatisticsResponse? = null + private var stats: MutableList = mutableListOf() private var type: MediaType = MediaType.ANIME private var statType: StatType = StatType.COUNT private lateinit var user: Query.UserProfile @@ -52,25 +52,55 @@ class StatsFragment() : binding.statisticList.isNestedScrollingEnabled = false binding.statisticList.layoutManager = LinearLayoutManager(requireContext()) binding.statisticProgressBar.visibility = View.VISIBLE + binding.compare.visibility = if (user.id == Anilist.userid) View.GONE else View.VISIBLE binding.sourceType.setAdapter( ArrayAdapter( requireContext(), R.layout.item_dropdown, - MediaType.entries.map { it.name.uppercase(Locale.ROOT) } + MediaType.entries.map { it.name.uppercase(Locale.ROOT).replace("_", " ") } ) ) binding.sourceFilter.setAdapter( ArrayAdapter( requireContext(), R.layout.item_dropdown, - StatType.entries.map { it.name.uppercase(Locale.ROOT) } + StatType.entries.map { it.name.uppercase(Locale.ROOT).replace("_", " ") } ) ) + binding.compare.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + activity.lifecycleScope.launch { + if (Anilist.userid != null) { + withContext(Dispatchers.Main) { + binding.statisticProgressBar.visibility = View.VISIBLE + binding.statisticList.visibility = View.GONE + } + val userStats = + Anilist.query.getUserStatistics(Anilist.userid!!)?.data?.user + if (userStats != null) { + stats.add(userStats) + withContext(Dispatchers.Main) { + loadStats(type == MediaType.ANIME) + binding.statisticProgressBar.visibility = View.GONE + binding.statisticList.visibility = View.VISIBLE + } + } + } + } + } else { + stats.removeAll( + stats.filter { it?.id == Anilist.userid } + ) + loadStats(type == MediaType.ANIME) + } + } + binding.filterContainer.visibility = View.GONE activity.lifecycleScope.launch { - stats = Anilist.query.getUserStatistics(user.id) + stats.clear() + stats.add(Anilist.query.getUserStatistics(user.id)?.data?.user) withContext(Dispatchers.Main) { binding.filterContainer.visibility = View.VISIBLE binding.sourceType.setOnItemClickListener { _, _, i, _ -> @@ -116,99 +146,114 @@ class StatsFragment() : } private fun loadFormatChart(anime: Boolean) { - val names: List = if (anime) { - stats?.data?.user?.statistics?.anime?.formats?.map { it.format } ?: emptyList() - } else { - stats?.data?.user?.statistics?.manga?.formats?.map { it.format } ?: emptyList() + val chartPackets = mutableListOf() + stats.forEach {stat -> + val names: List = if (anime) { + stat?.statistics?.anime?.formats?.map { it.format } ?: emptyList() + } else { + stat?.statistics?.manga?.formats?.map { it.format } ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stat?.statistics?.anime?.formats?.map { it.count } + StatType.TIME -> stat?.statistics?.anime?.formats?.map { it.minutesWatched / 60 } + StatType.AVG_SCORE -> stat?.statistics?.anime?.formats?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stat?.statistics?.manga?.formats?.map { it.count } + StatType.TIME -> stat?.statistics?.manga?.formats?.map { it.chaptersRead } + StatType.AVG_SCORE -> stat?.statistics?.manga?.formats?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() && values.isNotEmpty()) { + chartPackets.add(ChartPacket(stat?.name?:"Unknown", names, values)) + } } - val values: List = if (anime) { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.anime?.formats?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.anime?.formats?.map { it.minutesWatched / 60 } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.formats?.map { it.meanScore } - } ?: emptyList() - } else { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.manga?.formats?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.manga?.formats?.map { it.chaptersRead } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.formats?.map { it.meanScore } - } ?: emptyList() - } - if (names.isNotEmpty() || values.isNotEmpty()) { + if (chartPackets.isNotEmpty()) { val formatChart = ChartBuilder.buildChart( activity, ChartType.OneDimensional, AAChartType.Pie, statType, type, - names, - values + chartPackets, ) adapter.add(ChartItem("Format", formatChart, activity)) } } private fun loadStatusChart(anime: Boolean) { - val names: List = if (anime) { - stats?.data?.user?.statistics?.anime?.statuses?.map { it.status } ?: emptyList() - } else { - stats?.data?.user?.statistics?.manga?.statuses?.map { it.status } ?: emptyList() + val chartPackets = mutableListOf() + stats.forEach { stat -> + val names: List = if (anime) { + stat?.statistics?.anime?.statuses?.map { it.status } ?: emptyList() + } else { + stat?.statistics?.manga?.statuses?.map { it.status } ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stat?.statistics?.anime?.statuses?.map { it.count } + StatType.TIME -> stat?.statistics?.anime?.statuses?.map { it.minutesWatched / 60 } + StatType.AVG_SCORE -> stat?.statistics?.anime?.statuses?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stat?.statistics?.manga?.statuses?.map { it.count } + StatType.TIME -> stat?.statistics?.manga?.statuses?.map { it.chaptersRead } + StatType.AVG_SCORE -> stat?.statistics?.manga?.statuses?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() && values.isNotEmpty()) { + chartPackets.add(ChartPacket(stat?.name ?: "Unknown", names, values)) + } } - val values: List = if (anime) { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.anime?.statuses?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.anime?.statuses?.map { it.minutesWatched / 60 } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.statuses?.map { it.meanScore } - } ?: emptyList() - } else { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.manga?.statuses?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.manga?.statuses?.map { it.chaptersRead } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.statuses?.map { it.meanScore } - } ?: emptyList() - } - if (names.isNotEmpty() || values.isNotEmpty()) { + if (chartPackets.isNotEmpty()) { val statusChart = ChartBuilder.buildChart( activity, ChartType.OneDimensional, AAChartType.Funnel, statType, type, - names, - values + chartPackets ) adapter.add(ChartItem("Status", statusChart, activity)) } } private fun loadScoreChart(anime: Boolean) { - val names: List = if (anime) { - stats?.data?.user?.statistics?.anime?.scores?.map { it.score } ?: emptyList() - } else { - stats?.data?.user?.statistics?.manga?.scores?.map { it.score } ?: emptyList() + val chartPackets = mutableListOf() + stats.forEach { stat -> + val names: List = if (anime) { + stat?.statistics?.anime?.scores?.map { it.score } ?: emptyList() + } else { + stat?.statistics?.manga?.scores?.map { it.score } ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stat?.statistics?.anime?.scores?.map { it.count } + StatType.TIME -> stat?.statistics?.anime?.scores?.map { it.minutesWatched / 60 } + StatType.AVG_SCORE -> stat?.statistics?.anime?.scores?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stat?.statistics?.manga?.scores?.map { it.count } + StatType.TIME -> stat?.statistics?.manga?.scores?.map { it.chaptersRead } + StatType.AVG_SCORE -> stat?.statistics?.manga?.scores?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() || values.isNotEmpty()) { + chartPackets.add(ChartPacket(stat?.name ?: "Unknown", names, values)) + } } - val values: List = if (anime) { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.anime?.scores?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.anime?.scores?.map { it.minutesWatched / 60 } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.scores?.map { it.meanScore } - } ?: emptyList() - } else { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.manga?.scores?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.manga?.scores?.map { it.chaptersRead } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.scores?.map { it.meanScore } - } ?: emptyList() - } - if (names.isNotEmpty() || values.isNotEmpty()) { + if (chartPackets.isNotEmpty()) { val scoreChart = ChartBuilder.buildChart( activity, ChartType.TwoDimensional, AAChartType.Column, statType, type, - names, - values, + chartPackets, xAxisName = "Score", ) adapter.add(ChartItem("Score", scoreChart, activity)) @@ -216,35 +261,40 @@ class StatsFragment() : } private fun loadLengthChart(anime: Boolean) { - val names: List = if (anime) { - stats?.data?.user?.statistics?.anime?.lengths?.map { it.length ?: "unknown" } - ?: emptyList() - } else { - stats?.data?.user?.statistics?.manga?.lengths?.map { it.length ?: "unknown" } - ?: emptyList() + val chartPackets = mutableListOf() + stats.forEach { stat -> + val names: List = if (anime) { + stat?.statistics?.anime?.lengths?.map { it.length ?: "unknown" } + ?: emptyList() + } else { + stat?.statistics?.manga?.lengths?.map { it.length ?: "unknown" } + ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stat?.statistics?.anime?.lengths?.map { it.count } + StatType.TIME -> stat?.statistics?.anime?.lengths?.map { it.minutesWatched / 60 } + StatType.AVG_SCORE -> stat?.statistics?.anime?.lengths?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stat?.statistics?.manga?.lengths?.map { it.count } + StatType.TIME -> stat?.statistics?.manga?.lengths?.map { it.chaptersRead } + StatType.AVG_SCORE -> stat?.statistics?.manga?.lengths?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() || values.isNotEmpty()) { + chartPackets.add(ChartPacket(stat?.name ?: "Unknown", names, values)) + } } - val values: List = if (anime) { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.anime?.lengths?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.anime?.lengths?.map { it.minutesWatched / 60 } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.lengths?.map { it.meanScore } - } ?: emptyList() - } else { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.manga?.lengths?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.manga?.lengths?.map { it.chaptersRead } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.lengths?.map { it.meanScore } - } ?: emptyList() - } - if (names.isNotEmpty() || values.isNotEmpty()) { + if (chartPackets.isNotEmpty()) { val lengthChart = ChartBuilder.buildChart( activity, ChartType.OneDimensional, AAChartType.Pyramid, statType, type, - names, - values, + chartPackets, xAxisName = "Length", ) adapter.add(ChartItem("Length", lengthChart, activity)) @@ -252,35 +302,40 @@ class StatsFragment() : } private fun loadReleaseYearChart(anime: Boolean) { - val names: List = if (anime) { - stats?.data?.user?.statistics?.anime?.releaseYears?.map { it.releaseYear } - ?: emptyList() - } else { - stats?.data?.user?.statistics?.manga?.releaseYears?.map { it.releaseYear } - ?: emptyList() + val chartPackets = mutableListOf() + stats.forEach { stat -> + val names: List = if (anime) { + stat?.statistics?.anime?.releaseYears?.map { it.releaseYear } + ?: emptyList() + } else { + stat?.statistics?.manga?.releaseYears?.map { it.releaseYear } + ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stat?.statistics?.anime?.releaseYears?.map { it.count } + StatType.TIME -> stat?.statistics?.anime?.releaseYears?.map { it.minutesWatched / 60 } + StatType.AVG_SCORE -> stat?.statistics?.anime?.releaseYears?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stat?.statistics?.manga?.releaseYears?.map { it.count } + StatType.TIME -> stat?.statistics?.manga?.releaseYears?.map { it.chaptersRead } + StatType.AVG_SCORE -> stat?.statistics?.manga?.releaseYears?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() || values.isNotEmpty()) { + chartPackets.add(ChartPacket(stat?.name ?: "Unknown", names, values)) + } } - val values: List = if (anime) { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.anime?.releaseYears?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.anime?.releaseYears?.map { it.minutesWatched / 60 } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.releaseYears?.map { it.meanScore } - } ?: emptyList() - } else { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.manga?.releaseYears?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.manga?.releaseYears?.map { it.chaptersRead } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.releaseYears?.map { it.meanScore } - } ?: emptyList() - } - if (names.isNotEmpty() || values.isNotEmpty()) { + if (chartPackets.isNotEmpty()) { val releaseYearChart = ChartBuilder.buildChart( activity, ChartType.TwoDimensional, AAChartType.Bubble, statType, type, - names, - values, + chartPackets, xAxisName = "Year", ) adapter.add(ChartItem("Release Year", releaseYearChart, activity)) @@ -288,33 +343,38 @@ class StatsFragment() : } private fun loadStartYearChart(anime: Boolean) { - val names: List = if (anime) { - stats?.data?.user?.statistics?.anime?.startYears?.map { it.startYear } ?: emptyList() - } else { - stats?.data?.user?.statistics?.manga?.startYears?.map { it.startYear } ?: emptyList() + val chartPackets = mutableListOf() + stats.forEach { stat -> + val names: List = if (anime) { + stat?.statistics?.anime?.startYears?.map { it.startYear } ?: emptyList() + } else { + stat?.statistics?.manga?.startYears?.map { it.startYear } ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stat?.statistics?.anime?.startYears?.map { it.count } + StatType.TIME -> stat?.statistics?.anime?.startYears?.map { it.minutesWatched / 60 } + StatType.AVG_SCORE -> stat?.statistics?.anime?.startYears?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stat?.statistics?.manga?.startYears?.map { it.count } + StatType.TIME -> stat?.statistics?.manga?.startYears?.map { it.chaptersRead } + StatType.AVG_SCORE -> stat?.statistics?.manga?.startYears?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() || values.isNotEmpty()) { + chartPackets.add(ChartPacket(stat?.name ?: "Unknown", names, values)) + } } - val values: List = if (anime) { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.anime?.startYears?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.anime?.startYears?.map { it.minutesWatched / 60 } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.startYears?.map { it.meanScore } - } ?: emptyList() - } else { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.manga?.startYears?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.manga?.startYears?.map { it.chaptersRead } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.startYears?.map { it.meanScore } - } ?: emptyList() - } - if (names.isNotEmpty() || values.isNotEmpty()) { + if (chartPackets.isNotEmpty()) { val startYearChart = ChartBuilder.buildChart( activity, ChartType.TwoDimensional, AAChartType.Bar, statType, type, - names, - values, + chartPackets, xAxisName = "Year", ) adapter.add(ChartItem("Start Year", startYearChart, activity)) @@ -322,112 +382,160 @@ class StatsFragment() : } private fun loadGenreChart(anime: Boolean) { - val names: List = if (anime) { - stats?.data?.user?.statistics?.anime?.genres?.map { it.genre } ?: emptyList() - } else { - stats?.data?.user?.statistics?.manga?.genres?.map { it.genre } ?: emptyList() + val chartPackets = mutableListOf() + stats.forEach { stat -> + val names: List = if (anime) { + stat?.statistics?.anime?.genres?.map { it.genre } ?: emptyList() + } else { + stat?.statistics?.manga?.genres?.map { it.genre } ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stat?.statistics?.anime?.genres?.map { it.count } + StatType.TIME -> stat?.statistics?.anime?.genres?.map { it.minutesWatched / 60 } + StatType.AVG_SCORE -> stat?.statistics?.anime?.genres?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stat?.statistics?.manga?.genres?.map { it.count } + StatType.TIME -> stat?.statistics?.manga?.genres?.map { it.chaptersRead } + StatType.AVG_SCORE -> stat?.statistics?.manga?.genres?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() || values.isNotEmpty()) { + chartPackets.add(ChartPacket(stat?.name ?: "Unknown", names, values)) + } } - val values: List = if (anime) { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.anime?.genres?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.anime?.genres?.map { it.minutesWatched / 60 } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.genres?.map { it.meanScore } - } ?: emptyList() - } else { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.manga?.genres?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.manga?.genres?.map { it.chaptersRead } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.genres?.map { it.meanScore } - } ?: emptyList() - } - if (names.isNotEmpty() || values.isNotEmpty()) { + if (chartPackets.isNotEmpty()) { + val referenceNames = chartPackets.first().names.map { it.toString() } + val standardizedPackets = chartPackets.map { packet -> + val valuesMap = packet.names.map { it.toString() }.zip(packet.statData).toMap() + val standardizedValues = referenceNames.map { name -> + valuesMap[name] ?: 0 + } + + // Create a new ChartPacket with standardized names and values. + ChartPacket(packet.username, referenceNames, standardizedValues) + }.toMutableList() + chartPackets.clear() + chartPackets.addAll(standardizedPackets) val genreChart = ChartBuilder.buildChart( activity, ChartType.TwoDimensional, AAChartType.Areaspline, statType, type, - names, - values, + chartPackets, xAxisName = "Genre", polar = true, - categories = names + passedCategories = chartPackets[0].names as List, ) adapter.add(ChartItem("Genre", genreChart, activity)) } } private fun loadTagChart(anime: Boolean) { - val names: List = if (anime) { - stats?.data?.user?.statistics?.anime?.tags?.map { it.tag.name } ?: emptyList() - } else { - stats?.data?.user?.statistics?.manga?.tags?.map { it.tag.name } ?: emptyList() + val chartPackets = mutableListOf() + stats.forEach { stat -> + val names: List = if (anime) { + stat?.statistics?.anime?.tags?.map { it.tag.name } ?: emptyList() + } else { + stat?.statistics?.manga?.tags?.map { it.tag.name } ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stat?.statistics?.anime?.tags?.map { it.count } + StatType.TIME -> stat?.statistics?.anime?.tags?.map { it.minutesWatched / 60 } + StatType.AVG_SCORE -> stat?.statistics?.anime?.tags?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stat?.statistics?.manga?.tags?.map { it.count } + StatType.TIME -> stat?.statistics?.manga?.tags?.map { it.chaptersRead } + StatType.AVG_SCORE -> stat?.statistics?.manga?.tags?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() || values.isNotEmpty()) { + chartPackets.add(ChartPacket(stat?.name ?: "Unknown", names, values)) + } } - val values: List = if (anime) { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.anime?.tags?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.anime?.tags?.map { it.minutesWatched / 60 } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.tags?.map { it.meanScore } - } ?: emptyList() - } else { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.manga?.tags?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.manga?.tags?.map { it.chaptersRead } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.tags?.map { it.meanScore } - } ?: emptyList() - } - if (names.isNotEmpty() || values.isNotEmpty()) { - val min = values.minOf { it.toInt() } - val max = values.maxOf { it.toInt() } + if (chartPackets.isNotEmpty()) { + val referenceNames = chartPackets.first().names.map { it.toString() } + val standardizedPackets = chartPackets.map { packet -> + val valuesMap = packet.names.map { it.toString() }.zip(packet.statData).toMap() + val standardizedValues = referenceNames.map { name -> + valuesMap[name] ?: 0 + } + + // Create a new ChartPacket with standardized names and values. + ChartPacket(packet.username, referenceNames, standardizedValues) + }.toMutableList() + chartPackets.clear() + chartPackets.addAll(standardizedPackets) val tagChart = ChartBuilder.buildChart( activity, ChartType.TwoDimensional, AAChartType.Areaspline, statType, type, - names, - values, + chartPackets, xAxisName = "Tag", polar = false, - categories = names, + passedCategories = chartPackets[0].names as List, scrollPos = 0.0f ) - tagChart.yAxis = AAYAxis().min(min).max(max).tickInterval(if (max > 100) 20 else 10) adapter.add(ChartItem("Tag", tagChart, activity)) } } private fun loadCountryChart(anime: Boolean) { - val names: List = if (anime) { - stats?.data?.user?.statistics?.anime?.countries?.map { it.country } ?: emptyList() - } else { - stats?.data?.user?.statistics?.manga?.countries?.map { it.country } ?: emptyList() + val chartPackets = mutableListOf() + stats.forEach { stat -> + val names: List = if (anime) { + stat?.statistics?.anime?.countries?.map { it.country } ?: emptyList() + } else { + stat?.statistics?.manga?.countries?.map { it.country } ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stat?.statistics?.anime?.countries?.map { it.count } + StatType.TIME -> stat?.statistics?.anime?.countries?.map { it.minutesWatched / 60 } + StatType.AVG_SCORE -> stat?.statistics?.anime?.countries?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stat?.statistics?.manga?.countries?.map { it.count } + StatType.TIME -> stat?.statistics?.manga?.countries?.map { it.chaptersRead } + StatType.AVG_SCORE -> stat?.statistics?.manga?.countries?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() || values.isNotEmpty()) { + chartPackets.add(ChartPacket(stat?.name ?: "Unknown", names, values)) + } } - val values: List = if (anime) { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.anime?.countries?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.anime?.countries?.map { it.minutesWatched / 60 } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.countries?.map { it.meanScore } - } ?: emptyList() - } else { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.manga?.countries?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.manga?.countries?.map { it.chaptersRead } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.countries?.map { it.meanScore } - } ?: emptyList() - } - if (names.isNotEmpty() || values.isNotEmpty()) { + if (chartPackets.isNotEmpty()) { + val referenceNames = chartPackets.first().names.map { it.toString() } + val standardizedPackets = chartPackets.map { packet -> + val valuesMap = packet.names.map { it.toString() }.zip(packet.statData).toMap() + val standardizedValues = referenceNames.map { name -> + valuesMap[name] ?: 0 + } + + // Create a new ChartPacket with standardized names and values. + ChartPacket(packet.username, referenceNames, standardizedValues) + }.toMutableList() + chartPackets.clear() + chartPackets.addAll(standardizedPackets) val countryChart = ChartBuilder.buildChart( activity, ChartType.OneDimensional, AAChartType.Pie, statType, type, - names, - values, + chartPackets, xAxisName = "Country", polar = false, - categories = names, + passedCategories = chartPackets[0].names as List, scrollPos = null ) adapter.add(ChartItem("Country", countryChart, activity)) @@ -435,116 +543,167 @@ class StatsFragment() : } private fun loadVoiceActorsChart(anime: Boolean) { - val names: List = if (anime) { - stats?.data?.user?.statistics?.anime?.voiceActors?.map { it.voiceActor.name.full?:"unknown" } ?: emptyList() - } else { - stats?.data?.user?.statistics?.manga?.voiceActors?.map { it.voiceActor.name.full?:"unknown" } ?: emptyList() + val chartPackets = mutableListOf() + stats.forEach { stat -> + val names: List = if (anime) { + stat?.statistics?.anime?.voiceActors?.map { it.voiceActor.name.full?:"unknown" } ?: emptyList() + } else { + stat?.statistics?.manga?.voiceActors?.map { it.voiceActor.name.full?:"unknown" } ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stat?.statistics?.anime?.voiceActors?.map { it.count } + StatType.TIME -> stat?.statistics?.anime?.voiceActors?.map { it.minutesWatched / 60 } + StatType.AVG_SCORE -> stat?.statistics?.anime?.voiceActors?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stat?.statistics?.manga?.voiceActors?.map { it.count } + StatType.TIME -> stat?.statistics?.manga?.voiceActors?.map { it.chaptersRead } + StatType.AVG_SCORE -> stat?.statistics?.manga?.voiceActors?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() || values.isNotEmpty()) { + chartPackets.add(ChartPacket(stat?.name ?: "Unknown", names, values)) + } } - val values: List = if (anime) { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.anime?.voiceActors?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.anime?.voiceActors?.map { it.minutesWatched / 60 } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.voiceActors?.map { it.meanScore } - } ?: emptyList() - } else { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.manga?.voiceActors?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.manga?.voiceActors?.map { it.chaptersRead } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.voiceActors?.map { it.meanScore } - } ?: emptyList() - } - if (names.isNotEmpty() || values.isNotEmpty()) { + if (chartPackets.isNotEmpty()) { + val referenceNames = chartPackets.first().names.map { it.toString() } + val standardizedPackets = chartPackets.map { packet -> + val valuesMap = packet.names.map { it.toString() }.zip(packet.statData).toMap() + val standardizedValues = referenceNames.map { name -> + valuesMap[name] ?: 0 + } + + // Create a new ChartPacket with standardized names and values. + ChartPacket(packet.username, referenceNames, standardizedValues) + }.toMutableList() + chartPackets.clear() + chartPackets.addAll(standardizedPackets) val voiceActorsChart = ChartBuilder.buildChart( activity, ChartType.TwoDimensional, AAChartType.Column, statType, type, - names, - values, + chartPackets, xAxisName = "Voice Actor", polar = false, - categories = names, + passedCategories = chartPackets[0].names as List, scrollPos = 0.0f ) adapter.add(ChartItem("Voice Actor", voiceActorsChart, activity)) } } - private fun loadStaffChart(anime: Boolean) { - val names: List = if (anime) { - stats?.data?.user?.statistics?.anime?.staff?.map { it.staff.name.full?:"unknown" } ?: emptyList() - } else { - stats?.data?.user?.statistics?.manga?.staff?.map { it.staff.name.full?:"unknown" } ?: emptyList() - } - val values: List = if (anime) { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.anime?.staff?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.anime?.staff?.map { it.minutesWatched / 60 } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.staff?.map { it.meanScore } - } ?: emptyList() - } else { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.manga?.staff?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.manga?.staff?.map { it.chaptersRead } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.staff?.map { it.meanScore } - } ?: emptyList() - } - if (names.isNotEmpty() || values.isNotEmpty()) { - val staffChart = ChartBuilder.buildChart( - activity, - ChartType.TwoDimensional, - AAChartType.Line, - statType, - type, - names, - values, - xAxisName = "Staff", - polar = false, - categories = names, - scrollPos = 0.0f - ) - adapter.add(ChartItem("Staff", staffChart, activity)) - } - } - private fun loadStudioChart(anime: Boolean) { - val names: List = if (anime) { - stats?.data?.user?.statistics?.anime?.studios?.map { it.studio.name } ?: emptyList() - } else { - stats?.data?.user?.statistics?.manga?.studios?.map { it.studio.name } ?: emptyList() + val chartPackets = mutableListOf() + stats.forEach { stat -> + val names: List = if (anime) { + stat?.statistics?.anime?.studios?.map { it.studio.name } ?: emptyList() + } else { + stat?.statistics?.manga?.studios?.map { it.studio.name } ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stat?.statistics?.anime?.studios?.map { it.count } + StatType.TIME -> stat?.statistics?.anime?.studios?.map { it.minutesWatched / 60 } + StatType.AVG_SCORE -> stat?.statistics?.anime?.studios?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stat?.statistics?.manga?.studios?.map { it.count } + StatType.TIME -> stat?.statistics?.manga?.studios?.map { it.chaptersRead } + StatType.AVG_SCORE -> stat?.statistics?.manga?.studios?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() || values.isNotEmpty()) { + chartPackets.add(ChartPacket(stat?.name ?: "Unknown", names, values)) + } } - val values: List = if (anime) { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.anime?.studios?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.anime?.studios?.map { it.minutesWatched / 60 } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.studios?.map { it.meanScore } - } ?: emptyList() - } else { - when (statType) { - StatType.COUNT -> stats?.data?.user?.statistics?.manga?.studios?.map { it.count } - StatType.TIME -> stats?.data?.user?.statistics?.manga?.studios?.map { it.chaptersRead } - StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.studios?.map { it.meanScore } - } ?: emptyList() - } - if (names.isNotEmpty() || values.isNotEmpty()) { + if (chartPackets.isNotEmpty()) { + val referenceNames = chartPackets.first().names.map { it.toString() } + val standardizedPackets = chartPackets.map { packet -> + val valuesMap = packet.names.map { it.toString() }.zip(packet.statData).toMap() + val standardizedValues = referenceNames.map { name -> + valuesMap[name] ?: 0 + } + + // Create a new ChartPacket with standardized names and values. + ChartPacket(packet.username, referenceNames, standardizedValues) + }.toMutableList() + chartPackets.clear() + chartPackets.addAll(standardizedPackets) val studioChart = ChartBuilder.buildChart( activity, ChartType.TwoDimensional, AAChartType.Spline, statType, type, - names.take(15), - values.take(15), + chartPackets, xAxisName = "Studio", polar = true, - categories = names, + passedCategories = chartPackets[0].names as List, scrollPos = null ) adapter.add(ChartItem("Studio", studioChart, activity)) } } + private fun loadStaffChart(anime: Boolean) { + val chartPackets = mutableListOf() + stats.forEach { stat -> + val names: List = if (anime) { + stat?.statistics?.anime?.staff?.map { it.staff.name.full?:"unknown" } ?: emptyList() + } else { + stat?.statistics?.manga?.staff?.map { it.staff.name.full?:"unknown" } ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stat?.statistics?.anime?.staff?.map { it.count } + StatType.TIME -> stat?.statistics?.anime?.staff?.map { it.minutesWatched / 60 } + StatType.AVG_SCORE -> stat?.statistics?.anime?.staff?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stat?.statistics?.manga?.staff?.map { it.count } + StatType.TIME -> stat?.statistics?.manga?.staff?.map { it.chaptersRead } + StatType.AVG_SCORE -> stat?.statistics?.manga?.staff?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() || values.isNotEmpty()) { + chartPackets.add(ChartPacket(stat?.name ?: "Unknown", names, values)) + } + } + if (chartPackets.isNotEmpty()) { + val referenceNames = chartPackets.first().names.map { it.toString() } + val standardizedPackets = chartPackets.map { packet -> + val valuesMap = packet.names.map { it.toString() }.zip(packet.statData).toMap() + val standardizedValues = referenceNames.map { name -> + valuesMap[name] ?: 0 + } + + // Create a new ChartPacket with standardized names and values. + ChartPacket(packet.username, referenceNames, standardizedValues) + }.toMutableList() + chartPackets.clear() + chartPackets.addAll(standardizedPackets) + val staffChart = ChartBuilder.buildChart( + activity, + ChartType.TwoDimensional, + AAChartType.Line, + statType, + type, + chartPackets, + xAxisName = "Staff", + polar = false, + passedCategories = chartPackets[0].names as List, + scrollPos = 0.0f + ) + adapter.add(ChartItem("Staff", staffChart, activity)) + } + } + companion object { fun newInstance(user: Query.UserProfile): StatsFragment { val args = Bundle().apply { diff --git a/app/src/main/java/ani/dantotsu/util/ColorEditor.kt b/app/src/main/java/ani/dantotsu/util/ColorEditor.kt new file mode 100644 index 00000000..4a2c1d45 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/util/ColorEditor.kt @@ -0,0 +1,88 @@ +package ani.dantotsu.util + +import android.graphics.Color +import kotlin.math.pow + +class ColorEditor { + companion object { + fun oppositeColor(color: Int): Int { + val hsv = FloatArray(3) + Color.colorToHSV(color, hsv) + hsv[0] = (hsv[0] + 180) % 360 + return adjustColorForContrast(Color.HSVToColor(hsv), color) + } + + fun generateColorPalette( + baseColor: Int, + size: Int, + hueDelta: Float = 8f, + saturationDelta: Float = 2.02f, + valueDelta: Float = 2.02f + ): List { + val palette = mutableListOf() + val hsv = FloatArray(3) + Color.colorToHSV(baseColor, hsv) + + for (i in 0 until size) { + val newHue = + (hsv[0] + hueDelta * i) % 360 // Ensure hue stays within the 0-360 range + val newSaturation = (hsv[1] + saturationDelta * i).coerceIn(0f, 1f) + val newValue = (hsv[2] + valueDelta * i).coerceIn(0f, 1f) + + val newHsv = floatArrayOf(newHue, newSaturation, newValue) + palette.add(Color.HSVToColor(newHsv)) + } + + return palette + } + + fun getLuminance(color: Int): Double { + val r = Color.red(color) / 255.0 + val g = Color.green(color) / 255.0 + val b = Color.blue(color) / 255.0 + + val rL = if (r <= 0.03928) r / 12.92 else ((r + 0.055) / 1.055).pow(2.4) + val gL = if (g <= 0.03928) g / 12.92 else ((g + 0.055) / 1.055).pow(2.4) + val bL = if (b <= 0.03928) b / 12.92 else ((b + 0.055) / 1.055).pow(2.4) + + return 0.2126 * rL + 0.7152 * gL + 0.0722 * bL + } + + fun getContrastRatio(color1: Int, color2: Int): Double { + val l1 = getLuminance(color1) + val l2 = getLuminance(color2) + + return if (l1 > l2) (l1 + 0.05) / (l2 + 0.05) else (l2 + 0.05) / (l1 + 0.05) + } + + fun adjustColorForContrast(originalColor: Int, backgroundColor: Int): Int { + var adjustedColor = originalColor + var contrastRatio = getContrastRatio(adjustedColor, backgroundColor) + val isBackgroundDark = getLuminance(backgroundColor) < 0.5 + + while (contrastRatio < 4.5) { + // Adjust brightness by modifying the RGB values + val r = Color.red(adjustedColor) + val g = Color.green(adjustedColor) + val b = Color.blue(adjustedColor) + + // Calculate the amount to adjust + val adjustment = if (isBackgroundDark) 10 else -10 + + // Adjust the color + val newR = (r + adjustment).coerceIn(0, 255) + val newG = (g + adjustment).coerceIn(0, 255) + val newB = (b + adjustment).coerceIn(0, 255) + + adjustedColor = Color.rgb(newR, newG, newB) + contrastRatio = getContrastRatio(adjustedColor, backgroundColor) + + // Break the loop if the color adjustment does not change (to avoid infinite loop) + if (newR == r && newG == g && newB == b) { + break + } + } + return adjustedColor + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_statistics.xml b/app/src/main/res/layout/fragment_statistics.xml index 4c9cb775..19aaff14 100644 --- a/app/src/main/res/layout/fragment_statistics.xml +++ b/app/src/main/res/layout/fragment_statistics.xml @@ -13,74 +13,96 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:baselineAligned="false" - android:orientation="horizontal" + android:orientation="vertical" android:padding="16dp"> - + - - + android:hint="Type" + android:paddingEnd="8dp" + app:boxCornerRadiusBottomEnd="8dp" + app:boxCornerRadiusBottomStart="8dp" + app:boxCornerRadiusTopEnd="8dp" + app:boxCornerRadiusTopStart="8dp" + app:boxStrokeColor="@color/text_input_layout_stroke_color" + app:hintAnimationEnabled="true"> - + + - - + android:hint="View" + android:paddingEnd="8dp" + app:boxCornerRadiusBottomEnd="8dp" + app:boxCornerRadiusBottomStart="8dp" + app:boxCornerRadiusTopEnd="8dp" + app:boxCornerRadiusTopStart="8dp" + app:boxStrokeColor="@color/text_input_layout_stroke_color" + app:hintAnimationEnabled="true"> + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da14a6e5..3739542c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -665,6 +665,7 @@ Oldest Highest rated Lowest rated + Compare