diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt index 24c4b9461..b4931d916 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt @@ -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) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/Colors.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/KotatsuColors.kt similarity index 95% rename from app/src/main/kotlin/org/koitharu/kotatsu/core/util/Colors.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/KotatsuColors.kt index e60bb74d4..2eff473a3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/Colors.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/KotatsuColors.kt @@ -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)) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index 54851696e..2a134a95f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt index 2f9f2e667..ffd456380 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt @@ -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) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index d79a9fb73..77e79e46c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -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(null) val newChaptersCount = details.flatMapLatest { d -> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/info/LocalInfoDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/info/LocalInfoDialog.kt index 95ae313f6..27afdd571 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/info/LocalInfoDialog.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/info/LocalInfoDialog.kt @@ -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() { 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, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/userdata/StorageUsagePreference.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/userdata/StorageUsagePreference.kt index bb072537a..6f12d7069 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/userdata/StorageUsagePreference.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/userdata/StorageUsagePreference.kt @@ -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) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsRepository.kt index a968c41f3..b2737ff31 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsRepository.kt @@ -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 { val entities = db.getStatsDao().findAll(mangaId) val map = TreeMap() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsAD.kt index 2ad6fd752..05484795d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsAD.kt @@ -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 } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsActivity.kt index 1c5ac57c5..ddb4f8d5f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsActivity.kt @@ -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(), 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(), } 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 { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/sheet/MangaStatsSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/sheet/MangaStatsSheet.kt index 3cdf8a700..7c12c195c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/sheet/MangaStatsSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/sheet/MangaStatsSheet.kt @@ -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() { +class MangaStatsSheet : BaseAdaptiveSheet(), View.OnClickListener { private val viewModel: MangaStatsViewModel by viewModels() @@ -33,11 +34,19 @@ class MangaStatsSheet : BaseAdaptiveSheet() { 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) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/sheet/MangaStatsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/sheet/MangaStatsViewModel.kt index 8272ef096..f81b4a6bd 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/sheet/MangaStatsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/sheet/MangaStatsViewModel.kt @@ -30,6 +30,7 @@ class MangaStatsViewModel @Inject constructor( val stats = MutableStateFlow(emptyIntList()) val startDate = MutableStateFlow(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) + } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/views/BarChartView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/views/BarChartView.kt index 9c48ec934..73c8dc2e6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/views/BarChartView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/views/BarChartView.kt @@ -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() private val bars = ArrayList() 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) { - 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.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( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/views/PieChartView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/views/PieChartView.kt index 526728ccc..0f5cd770f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/views/PieChartView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/views/PieChartView.kt @@ -161,6 +161,7 @@ class PieChartView @JvmOverloads constructor( val label: String, val percent: Float, val color: Int, + val tag: Any?, ) interface OnSegmentClickListener { diff --git a/app/src/main/res/layout/sheet_stats_manga.xml b/app/src/main/res/layout/sheet_stats_manga.xml index 13088406f..1771ef23b 100644 --- a/app/src/main/res/layout/sheet_stats_manga.xml +++ b/app/src/main/res/layout/sheet_stats_manga.xml @@ -14,28 +14,69 @@ android:layout_height="wrap_content" app:title="@string/reading_stats" /> - + android:scrollIndicators="top"> - + - + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/opt_details.xml b/app/src/main/res/menu/opt_details.xml index 2d9562a45..c525f7c5c 100644 --- a/app/src/main/res/menu/opt_details.xml +++ b/app/src/main/res/menu/opt_details.xml @@ -37,6 +37,12 @@ android:title="@string/tracking" app:showAsAction="never" /> + + Day Three months There are no statistics for the selected period + Pages read: %s diff --git a/app/src/test/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt b/app/src/test/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt index 04627babf..2ce335749 100644 --- a/app/src/test/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt +++ b/app/src/test/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt @@ -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()