Stats chart for single manga
This commit is contained in:
@@ -12,12 +12,10 @@ import android.graphics.RectF
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.graphics.withClip
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.Colors
|
||||
import kotlin.math.absoluteValue
|
||||
import org.koitharu.kotatsu.core.util.KotatsuColors
|
||||
|
||||
class FaviconDrawable(
|
||||
context: Context,
|
||||
@@ -45,7 +43,7 @@ class FaviconDrawable(
|
||||
}
|
||||
paint.textAlign = Paint.Align.CENTER
|
||||
paint.isFakeBoldText = true
|
||||
colorForeground = MaterialColors.harmonize(Colors.random(name), colorBackground)
|
||||
colorForeground = MaterialColors.harmonize(KotatsuColors.random(name), colorBackground)
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
object Colors {
|
||||
object KotatsuColors {
|
||||
|
||||
@ColorInt
|
||||
fun segmentColor(context: Context, @AttrRes resId: Int): Int {
|
||||
@@ -28,7 +28,7 @@ object Colors {
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun of(context: Context, manga: Manga?): Int {
|
||||
fun ofManga(context: Context, manga: Manga?): Int {
|
||||
val color = if (manga != null) {
|
||||
val hue = (manga.id.absoluteValue % 360).toFloat()
|
||||
ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))
|
||||
@@ -138,7 +138,10 @@ class DetailsActivity :
|
||||
},
|
||||
),
|
||||
)
|
||||
viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.containerDetails, viewBinding.layoutBottom))
|
||||
viewModel.onActionDone.observeEvent(
|
||||
this,
|
||||
ReversibleActionObserver(viewBinding.containerDetails, viewBinding.layoutBottom),
|
||||
)
|
||||
viewModel.onShowTip.observeEvent(this) { showTip() }
|
||||
viewModel.historyInfo.observe(this, ::onHistoryChanged)
|
||||
viewModel.selectedBranch.observe(this) {
|
||||
@@ -150,6 +153,7 @@ class DetailsActivity :
|
||||
viewModel.isChaptersEmpty.observe(this, chaptersMenuInvalidator)
|
||||
val menuInvalidator = MenuInvalidator(this)
|
||||
viewModel.favouriteCategories.observe(this, menuInvalidator)
|
||||
viewModel.isStatsEnabled.observe(this, menuInvalidator)
|
||||
viewModel.remoteManga.observe(this, menuInvalidator)
|
||||
viewModel.branches.observe(this) {
|
||||
viewBinding.buttonDropdown.isVisible = it.size > 1
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
||||
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
|
||||
|
||||
class DetailsMenuProvider(
|
||||
private val activity: FragmentActivity,
|
||||
@@ -43,6 +44,7 @@ class DetailsMenuProvider(
|
||||
menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity)
|
||||
menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable
|
||||
menu.findItem(R.id.action_online).isVisible = viewModel.remoteManga.value != null
|
||||
menu.findItem(R.id.action_stats).isVisible = viewModel.isStatsEnabled.value
|
||||
menu.findItem(R.id.action_favourite).setIcon(
|
||||
if (viewModel.favouriteCategories.value) R.drawable.ic_heart else R.drawable.ic_heart_outline,
|
||||
)
|
||||
@@ -101,6 +103,12 @@ class DetailsMenuProvider(
|
||||
}
|
||||
}
|
||||
|
||||
R.id.action_stats -> {
|
||||
viewModel.manga.value?.let {
|
||||
MangaStatsSheet.show(activity.supportFragmentManager, it)
|
||||
}
|
||||
}
|
||||
|
||||
R.id.action_scrobbling -> {
|
||||
viewModel.manga.value?.let {
|
||||
ScrobblingSelectorSheet.show(activity.supportFragmentManager, it, null)
|
||||
|
||||
@@ -100,6 +100,10 @@ class DetailsViewModel @Inject constructor(
|
||||
val favouriteCategories = interactor.observeIsFavourite(mangaId)
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||
|
||||
val isStatsEnabled = settings.observeAsStateFlow(viewModelScope + Dispatchers.Default, AppSettings.KEY_STATS_ENABLED) {
|
||||
isStatsEnabled
|
||||
}
|
||||
|
||||
val remoteManga = MutableStateFlow<Manga?>(null)
|
||||
|
||||
val newChaptersCount = details.flatMapLatest { d ->
|
||||
|
||||
@@ -2,18 +2,11 @@ package org.koitharu.kotatsu.local.ui.info
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -21,16 +14,13 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView
|
||||
import org.koitharu.kotatsu.core.util.Colors
|
||||
import org.koitharu.kotatsu.core.util.KotatsuColors
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.ext.combine
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.DialogLocalInfoBinding
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.settings.userdata.StorageUsage
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -67,7 +57,7 @@ class LocalInfoDialog : AlertDialogFragment<DialogLocalInfoBinding>() {
|
||||
val total = size + available
|
||||
val segment = SegmentedBarView.Segment(
|
||||
percent = (size.toDouble() / total.toDouble()).toFloat(),
|
||||
color = Colors.segmentColor(view.context, materialR.attr.colorPrimary),
|
||||
color = KotatsuColors.segmentColor(view.context, materialR.attr.colorPrimary),
|
||||
)
|
||||
requireViewBinding().labelUsed.text = view.context.getString(
|
||||
R.string.memory_usage_pattern,
|
||||
|
||||
@@ -3,20 +3,15 @@ package org.koitharu.kotatsu.settings.userdata
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView
|
||||
import org.koitharu.kotatsu.core.util.Colors
|
||||
import org.koitharu.kotatsu.core.util.KotatsuColors
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.databinding.PreferenceMemoryUsageBinding
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -39,15 +34,15 @@ class StorageUsagePreference @JvmOverloads constructor(
|
||||
val binding = PreferenceMemoryUsageBinding.bind(holder.itemView)
|
||||
val storageSegment = SegmentedBarView.Segment(
|
||||
usage?.savedManga?.percent ?: 0f,
|
||||
Colors.segmentColor(context, materialR.attr.colorPrimary),
|
||||
KotatsuColors.segmentColor(context, materialR.attr.colorPrimary),
|
||||
)
|
||||
val pagesSegment = SegmentedBarView.Segment(
|
||||
usage?.pagesCache?.percent ?: 0f,
|
||||
Colors.segmentColor(context, materialR.attr.colorSecondary),
|
||||
KotatsuColors.segmentColor(context, materialR.attr.colorSecondary),
|
||||
)
|
||||
val otherSegment = SegmentedBarView.Segment(
|
||||
usage?.otherCache?.percent ?: 0f,
|
||||
Colors.segmentColor(context, materialR.attr.colorTertiary),
|
||||
KotatsuColors.segmentColor(context, materialR.attr.colorTertiary),
|
||||
)
|
||||
|
||||
with(binding) {
|
||||
|
||||
@@ -56,6 +56,10 @@ class StatsRepository @Inject constructor(
|
||||
time
|
||||
}
|
||||
|
||||
suspend fun getTotalPagesRead(mangaId: Long): Int {
|
||||
return db.getStatsDao().getReadPagesCount(mangaId)
|
||||
}
|
||||
|
||||
suspend fun getMangaTimeline(mangaId: Long): NavigableMap<Long, Int> {
|
||||
val entities = db.getStatsDao().findAll(mangaId)
|
||||
val map = TreeMap<Long, Int>()
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.content.res.ColorStateList
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.Colors
|
||||
import org.koitharu.kotatsu.core.util.KotatsuColors
|
||||
import org.koitharu.kotatsu.databinding.ItemStatsBinding
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.stats.domain.StatsRecord
|
||||
@@ -22,7 +22,7 @@ fun statsAD(
|
||||
bind {
|
||||
binding.textViewTitle.text = item.manga?.title ?: getString(R.string.other_manga)
|
||||
binding.textViewSummary.text = item.time.format(context.resources)
|
||||
binding.imageViewBadge.imageTintList = ColorStateList.valueOf(Colors.of(context, item.manga))
|
||||
binding.imageViewBadge.imageTintList = ColorStateList.valueOf(KotatsuColors.ofManga(context, item.manga))
|
||||
binding.root.isClickable = item.manga != null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,29 @@
|
||||
package org.koitharu.kotatsu.stats.ui
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.style.DynamicDrawableSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.ImageSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewStub
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.inSpans
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.util.Colors
|
||||
import org.koitharu.kotatsu.core.util.KotatsuColors
|
||||
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
@@ -43,7 +31,6 @@ import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
||||
import org.koitharu.kotatsu.core.util.ext.showOrHide
|
||||
import org.koitharu.kotatsu.databinding.ActivityStatsBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.stats.domain.StatsPeriod
|
||||
@@ -90,7 +77,8 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(),
|
||||
value = (v.duration / 1000).toInt(),
|
||||
label = v.manga?.title ?: getString(R.string.other_manga),
|
||||
percent = (v.duration.toDouble() / sum).toFloat(),
|
||||
color = Colors.of(this, v.manga),
|
||||
color = KotatsuColors.ofManga(this, v.manga),
|
||||
tag = v.manga,
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -105,9 +93,8 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(),
|
||||
}
|
||||
|
||||
override fun onSegmentClick(view: PieChartView, segment: PieChartView.Segment) {
|
||||
Toast.makeText(this, segment.label, Toast.LENGTH_SHORT).apply {
|
||||
setGravity(Gravity.TOP, 0, view.top + view.height / 2)
|
||||
}.show()
|
||||
val manga = segment.tag as? Manga ?: return
|
||||
onItemClick(manga, view)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
|
||||
@@ -2,27 +2,28 @@ package org.koitharu.kotatsu.stats.ui.sheet
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.collection.IntList
|
||||
import androidx.collection.LongIntMap
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.bookmarks.ui.sheet.BookmarksSheet
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.Colors
|
||||
import org.koitharu.kotatsu.core.util.KotatsuColors
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.SheetStatsMangaBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.format
|
||||
import org.koitharu.kotatsu.stats.ui.views.BarChartView
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MangaStatsSheet : BaseAdaptiveSheet<SheetStatsMangaBinding>() {
|
||||
class MangaStatsSheet : BaseAdaptiveSheet<SheetStatsMangaBinding>(), View.OnClickListener {
|
||||
|
||||
private val viewModel: MangaStatsViewModel by viewModels()
|
||||
|
||||
@@ -33,11 +34,19 @@ class MangaStatsSheet : BaseAdaptiveSheet<SheetStatsMangaBinding>() {
|
||||
override fun onViewBindingCreated(binding: SheetStatsMangaBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
binding.textViewTitle.text = viewModel.manga.title
|
||||
binding.chartView.barColor = Colors.of(binding.root.context, viewModel.manga)
|
||||
binding.chartView.barColor = KotatsuColors.ofManga(binding.root.context, viewModel.manga)
|
||||
viewModel.stats.observe(viewLifecycleOwner, ::onStatsChanged)
|
||||
viewModel.startDate.observe(viewLifecycleOwner) {
|
||||
binding.textViewStart.textAndVisible = it?.format(resources)
|
||||
}
|
||||
viewModel.totalPagesRead.observe(viewLifecycleOwner) {
|
||||
binding.textViewPages.text = getString(R.string.pages_read_s, it.format())
|
||||
}
|
||||
binding.buttonOpen.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
startActivity(DetailsActivity.newIntent(v.context, viewModel.manga))
|
||||
}
|
||||
|
||||
private fun onStatsChanged(stats: IntList) {
|
||||
|
||||
@@ -30,6 +30,7 @@ class MangaStatsViewModel @Inject constructor(
|
||||
|
||||
val stats = MutableStateFlow<IntList>(emptyIntList())
|
||||
val startDate = MutableStateFlow<DateTimeAgo?>(null)
|
||||
val totalPagesRead = MutableStateFlow(0)
|
||||
|
||||
init {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
@@ -39,7 +40,7 @@ class MangaStatsViewModel @Inject constructor(
|
||||
stats.value = emptyIntList()
|
||||
} else {
|
||||
val startDay = TimeUnit.MILLISECONDS.toDays(timeline.firstKey())
|
||||
val endDay = TimeUnit.MILLISECONDS.toDays(timeline.lastKey())
|
||||
val endDay = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis())
|
||||
val res = MutableIntList((endDay - startDay).toInt() + 1)
|
||||
for (day in startDay..endDay) {
|
||||
val from = TimeUnit.DAYS.toMillis(day)
|
||||
@@ -50,5 +51,8 @@ class MangaStatsViewModel @Inject constructor(
|
||||
startDate.value = calculateTimeAgo(Instant.ofEpochMilli(timeline.firstKey()))
|
||||
}
|
||||
}
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
totalPagesRead.value = repository.getTotalPagesRead(manga.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,12 @@ import com.google.android.material.color.MaterialColors
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||
import org.koitharu.kotatsu.parsers.util.replaceWith
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.random.Random
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class BarChartView @JvmOverloads constructor(
|
||||
@@ -35,10 +38,12 @@ class BarChartView @JvmOverloads constructor(
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val rawData = ArrayList<Bar>()
|
||||
private val bars = ArrayList<Bar>()
|
||||
private var maxValue: Int = 0
|
||||
private var spacing = context.resources.resolveDp(2f)
|
||||
private val minBarSpacing = context.resources.resolveDp(12f)
|
||||
private val minSpace = context.resources.resolveDp(20f)
|
||||
private val barWidth = context.resources.resolveDp(12f)
|
||||
private val outlineColor = context.getThemeColor(materialR.attr.colorOutline)
|
||||
private val dottedEffect = DashPathEffect(
|
||||
floatArrayOf(
|
||||
@@ -50,7 +55,7 @@ class BarChartView @JvmOverloads constructor(
|
||||
private val chartBounds = RectF()
|
||||
|
||||
@ColorInt
|
||||
var barColor: Int = Color.MAGENTA
|
||||
var barColor: Int = context.getThemeColor(materialR.attr.colorAccent)
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
@@ -58,6 +63,16 @@ class BarChartView @JvmOverloads constructor(
|
||||
|
||||
init {
|
||||
paint.strokeWidth = context.resources.resolveDp(1f)
|
||||
if (isInEditMode) {
|
||||
setData(
|
||||
List(Random.nextInt(20, 60)) {
|
||||
Bar(
|
||||
value = Random.nextInt(-20, 400).coerceAtLeast(0),
|
||||
label = it.toString(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
@@ -65,7 +80,7 @@ class BarChartView @JvmOverloads constructor(
|
||||
if (bars.isEmpty() || chartBounds.isEmpty) {
|
||||
return
|
||||
}
|
||||
val barWidth = ((chartBounds.width() + spacing) / bars.size.toFloat() - spacing)
|
||||
val spacing = (chartBounds.width() - (barWidth * bars.size.toFloat())) / (bars.size + 1).toFloat()
|
||||
// dashed horizontal lines
|
||||
paint.color = outlineColor
|
||||
paint.style = Paint.Style.STROKE
|
||||
@@ -75,19 +90,23 @@ class BarChartView @JvmOverloads constructor(
|
||||
val y = chartBounds.top + (chartBounds.height() * i / maxValue.toFloat())
|
||||
canvas.drawLine(paddingLeft.toFloat(), y, (width - paddingLeft - paddingRight).toFloat(), y, paint)
|
||||
}
|
||||
// bars
|
||||
paint.style = Paint.Style.FILL
|
||||
paint.color = barColor
|
||||
paint.pathEffect = null
|
||||
for ((i, bar) in bars.withIndex()) {
|
||||
val h = chartBounds.height() * bar.value / maxValue.toFloat()
|
||||
val x = i * (barWidth + spacing) + paddingLeft
|
||||
canvas.drawRect(x, chartBounds.bottom - h, x + barWidth, chartBounds.bottom, paint)
|
||||
}
|
||||
// bottom line
|
||||
paint.color = outlineColor
|
||||
paint.style = Paint.Style.STROKE
|
||||
canvas.drawLine(chartBounds.left, chartBounds.bottom, chartBounds.right, chartBounds.bottom, paint)
|
||||
// bars
|
||||
paint.style = Paint.Style.FILL
|
||||
paint.color = barColor
|
||||
paint.pathEffect = null
|
||||
val corner = barWidth / 2f
|
||||
for ((i, bar) in bars.withIndex()) {
|
||||
if (bar.value == 0) {
|
||||
continue
|
||||
}
|
||||
val h = (chartBounds.height() * bar.value / maxValue.toFloat()).coerceAtLeast(barWidth)
|
||||
val x = spacing + i * (barWidth + spacing) + paddingLeft
|
||||
canvas.drawRoundRect(x, chartBounds.bottom - h, x + barWidth, chartBounds.bottom, corner, corner, paint)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
@@ -96,11 +115,25 @@ class BarChartView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
fun setData(value: List<Bar>) {
|
||||
bars.replaceWith(value)
|
||||
maxValue = if (value.isEmpty()) 0 else value.maxOf { it.value }
|
||||
rawData.replaceWith(value)
|
||||
compressBars()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private fun compressBars() {
|
||||
if (rawData.isEmpty() || width <= 0) {
|
||||
maxValue = 0
|
||||
bars.clear()
|
||||
return
|
||||
}
|
||||
var fullWidth = rawData.size * (barWidth + minBarSpacing) + minBarSpacing
|
||||
val windowSize = (fullWidth / width.toFloat()).toIntUp()
|
||||
bars.replaceWith(
|
||||
rawData.chunked(windowSize) { it.average() },
|
||||
)
|
||||
maxValue = bars.maxOf { it.value }
|
||||
}
|
||||
|
||||
private fun computeValueStep(): Int {
|
||||
val h = chartBounds.height()
|
||||
var step = 1
|
||||
@@ -118,6 +151,18 @@ class BarChartView @JvmOverloads constructor(
|
||||
(width - paddingLeft - paddingRight).toFloat() - inset,
|
||||
(height - paddingTop - paddingBottom).toFloat() - inset,
|
||||
)
|
||||
compressBars()
|
||||
}
|
||||
|
||||
private fun Collection<Bar>.average(): Bar {
|
||||
return when (size) {
|
||||
0 -> Bar(0, "")
|
||||
1 -> first()
|
||||
else -> Bar(
|
||||
value = (sumOf { it.value } / size.toFloat()).roundToInt(),
|
||||
label = "%s - %s".format(first().label, last().label),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Bar(
|
||||
|
||||
@@ -161,6 +161,7 @@ class PieChartView @JvmOverloads constructor(
|
||||
val label: String,
|
||||
val percent: Float,
|
||||
val color: Int,
|
||||
val tag: Any?,
|
||||
)
|
||||
|
||||
interface OnSegmentClickListener {
|
||||
|
||||
@@ -14,28 +14,69 @@
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/reading_stats" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="@dimen/screen_padding"
|
||||
android:textAppearance="?textAppearanceTitleMedium"
|
||||
tools:text="@tools:sample/lorem[4]" />
|
||||
android:scrollIndicators="top">
|
||||
|
||||
<org.koitharu.kotatsu.stats.ui.views.BarChartView
|
||||
android:id="@+id/chartView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="240dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:paddingHorizontal="@dimen/screen_padding" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_start"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:paddingHorizontal="@dimen/screen_padding"
|
||||
android:textAppearance="?textAppearanceLabelSmall"
|
||||
tools:text="Week ago" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/screen_padding">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textAppearance="?textAppearanceTitleMedium"
|
||||
tools:text="@tools:sample/lorem[4]" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_open"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:minWidth="?minTouchTargetSize"
|
||||
android:minHeight="?minTouchTargetSize"
|
||||
app:srcCompat="@drawable/ic_open_external" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<org.koitharu.kotatsu.stats.ui.views.BarChartView
|
||||
android:id="@+id/chartView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="240dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:paddingHorizontal="@dimen/screen_padding" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_start"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:paddingHorizontal="@dimen/screen_padding"
|
||||
android:textAppearance="?textAppearanceLabelSmall"
|
||||
tools:text="Week ago" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_pages"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:paddingHorizontal="@dimen/screen_padding"
|
||||
android:textAppearance="?textAppearanceBodyMedium"
|
||||
tools:text="Total pages read: 250" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -37,6 +37,12 @@
|
||||
android:title="@string/tracking"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_stats"
|
||||
android:orderInCategory="50"
|
||||
android:title="@string/statistics"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_related"
|
||||
android:orderInCategory="50"
|
||||
|
||||
@@ -617,4 +617,5 @@
|
||||
<string name="day">Day</string>
|
||||
<string name="three_months">Three months</string>
|
||||
<string name="empty_stats_text">There are no statistics for the selected period</string>
|
||||
<string name="pages_read_s">Pages read: %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -73,6 +73,7 @@ class JsonSerializerTest {
|
||||
scroll = 24.0f,
|
||||
percent = 0.6f,
|
||||
deletedAt = 0L,
|
||||
chaptersCount = 12,
|
||||
)
|
||||
val json = JsonSerializer(entity).toJson()
|
||||
val result = JsonDeserializer(json).toHistoryEntity()
|
||||
|
||||
Reference in New Issue
Block a user