Timeline stats per manga
This commit is contained in:
@@ -7,6 +7,7 @@ import androidx.core.graphics.ColorUtils
|
|||||||
import com.google.android.material.R
|
import com.google.android.material.R
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
object Colors {
|
object Colors {
|
||||||
@@ -20,11 +21,24 @@ object Colors {
|
|||||||
return MaterialColors.harmonize(color, backgroundColor)
|
return MaterialColors.harmonize(color, backgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
fun random(seed: Any): Int {
|
fun random(seed: Any): Int {
|
||||||
val hue = (seed.hashCode() % 360).absoluteValue.toFloat()
|
val hue = (seed.hashCode() % 360).absoluteValue.toFloat()
|
||||||
return ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))
|
return ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
fun of(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))
|
||||||
|
} else {
|
||||||
|
context.getThemeColor(R.attr.colorSurface)
|
||||||
|
}
|
||||||
|
val backgroundColor = context.getThemeColor(R.attr.colorSurfaceContainerHigh)
|
||||||
|
return MaterialColors.harmonize(color, backgroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getHue(hex: String): Float {
|
private fun getHue(hex: String): Float {
|
||||||
val r = (hex.substring(0, 2).toInt(16)).toFloat()
|
val r = (hex.substring(0, 2).toInt(16)).toFloat()
|
||||||
val g = (hex.substring(2, 4).toInt(16)).toFloat()
|
val g = (hex.substring(2, 4).toInt(16)).toFloat()
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
package org.koitharu.kotatsu.stats.data
|
package org.koitharu.kotatsu.stats.data
|
||||||
|
|
||||||
|
import androidx.collection.LongIntMap
|
||||||
|
import androidx.collection.MutableLongIntMap
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||||
import org.koitharu.kotatsu.stats.domain.StatsPeriod
|
import org.koitharu.kotatsu.stats.domain.StatsPeriod
|
||||||
import org.koitharu.kotatsu.stats.domain.StatsRecord
|
import org.koitharu.kotatsu.stats.domain.StatsRecord
|
||||||
|
import java.util.NavigableMap
|
||||||
|
import java.util.TreeMap
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -19,13 +23,14 @@ class StatsRepository @Inject constructor(
|
|||||||
System.currentTimeMillis() - TimeUnit.DAYS.toMillis(period.days.toLong())
|
System.currentTimeMillis() - TimeUnit.DAYS.toMillis(period.days.toLong())
|
||||||
}
|
}
|
||||||
val stats = db.getStatsDao().getDurationStats(fromDate)
|
val stats = db.getStatsDao().getDurationStats(fromDate)
|
||||||
val minute = TimeUnit.MINUTES.toMillis(1)
|
|
||||||
val mangaDao = db.getMangaDao()
|
val mangaDao = db.getMangaDao()
|
||||||
val result = ArrayList<StatsRecord>(stats.size)
|
val result = ArrayList<StatsRecord>(stats.size)
|
||||||
var other = StatsRecord(null, 0)
|
var other = StatsRecord(null, 0)
|
||||||
|
val total = stats.values.sum()
|
||||||
for ((mangaId, duration) in stats) {
|
for ((mangaId, duration) in stats) {
|
||||||
val manga = mangaDao.find(mangaId)?.toManga()
|
val manga = mangaDao.find(mangaId)?.toManga()
|
||||||
if (manga == null || duration < minute) {
|
val percent = duration.toDouble() / total
|
||||||
|
if (manga == null || percent < 0.05) {
|
||||||
other = other.copy(duration = other.duration + duration)
|
other = other.copy(duration = other.duration + duration)
|
||||||
} else {
|
} else {
|
||||||
result += StatsRecord(
|
result += StatsRecord(
|
||||||
@@ -51,6 +56,15 @@ class StatsRepository @Inject constructor(
|
|||||||
time
|
time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getMangaTimeline(mangaId: Long): NavigableMap<Long, Int> {
|
||||||
|
val entities = db.getStatsDao().findAll(mangaId)
|
||||||
|
val map = TreeMap<Long, Int>()
|
||||||
|
for (e in entities) {
|
||||||
|
map[e.startedAt] = e.pages
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun clearStats() {
|
suspend fun clearStats() {
|
||||||
db.getStatsDao().clear()
|
db.getStatsDao().clear()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,16 +32,4 @@ data class StatsRecord(
|
|||||||
isContinue = false,
|
isContinue = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
fun getColor(context: Context): Int {
|
|
||||||
val color = if (manga != null) {
|
|
||||||
val hue = (manga.id.absoluteValue % 360).toFloat()
|
|
||||||
ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))
|
|
||||||
} else {
|
|
||||||
context.getThemeColor(R.attr.colorSurface)
|
|
||||||
}
|
|
||||||
val backgroundColor = context.getThemeColor(R.attr.colorSurfaceContainerHigh)
|
|
||||||
return MaterialColors.harmonize(color, backgroundColor)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.res.ColorStateList
|
|||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.util.Colors
|
||||||
import org.koitharu.kotatsu.databinding.ItemStatsBinding
|
import org.koitharu.kotatsu.databinding.ItemStatsBinding
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.stats.domain.StatsRecord
|
import org.koitharu.kotatsu.stats.domain.StatsRecord
|
||||||
@@ -21,7 +22,7 @@ fun statsAD(
|
|||||||
bind {
|
bind {
|
||||||
binding.textViewTitle.text = item.manga?.title ?: getString(R.string.other_manga)
|
binding.textViewTitle.text = item.manga?.title ?: getString(R.string.other_manga)
|
||||||
binding.textViewSummary.text = item.time.format(context.resources)
|
binding.textViewSummary.text = item.time.format(context.resources)
|
||||||
binding.imageViewBadge.imageTintList = ColorStateList.valueOf(item.getColor(context))
|
binding.imageViewBadge.imageTintList = ColorStateList.valueOf(Colors.of(context, item.manga))
|
||||||
binding.root.isClickable = item.manga != null
|
binding.root.isClickable = item.manga != null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import org.koitharu.kotatsu.core.ui.BaseFragment
|
|||||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||||
|
import org.koitharu.kotatsu.core.util.Colors
|
||||||
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
|
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.enqueueWith
|
||||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||||
@@ -47,6 +48,7 @@ import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
|||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.stats.domain.StatsPeriod
|
import org.koitharu.kotatsu.stats.domain.StatsPeriod
|
||||||
import org.koitharu.kotatsu.stats.domain.StatsRecord
|
import org.koitharu.kotatsu.stats.domain.StatsRecord
|
||||||
|
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
|
||||||
import org.koitharu.kotatsu.stats.ui.views.PieChartView
|
import org.koitharu.kotatsu.stats.ui.views.PieChartView
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -88,7 +90,7 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(),
|
|||||||
value = (v.duration / 1000).toInt(),
|
value = (v.duration / 1000).toInt(),
|
||||||
label = v.manga?.title ?: getString(R.string.other_manga),
|
label = v.manga?.title ?: getString(R.string.other_manga),
|
||||||
percent = (v.duration.toDouble() / sum).toFloat(),
|
percent = (v.duration.toDouble() / sum).toFloat(),
|
||||||
color = v.getColor(this),
|
color = Colors.of(this, v.manga),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -99,7 +101,7 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(),
|
|||||||
override fun onWindowInsetsChanged(insets: Insets) = Unit
|
override fun onWindowInsetsChanged(insets: Insets) = Unit
|
||||||
|
|
||||||
override fun onItemClick(item: Manga, view: View) {
|
override fun onItemClick(item: Manga, view: View) {
|
||||||
startActivity(DetailsActivity.newIntent(view.context, item))
|
MangaStatsSheet.show(supportFragmentManager, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSegmentClick(view: PieChartView, segment: PieChartView.Segment) {
|
override fun onSegmentClick(view: PieChartView, segment: PieChartView.Segment) {
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package org.koitharu.kotatsu.stats.ui.sheet
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
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.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.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.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.stats.ui.views.BarChartView
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class MangaStatsSheet : BaseAdaptiveSheet<SheetStatsMangaBinding>() {
|
||||||
|
|
||||||
|
private val viewModel: MangaStatsViewModel by viewModels()
|
||||||
|
|
||||||
|
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetStatsMangaBinding {
|
||||||
|
return SheetStatsMangaBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
viewModel.stats.observe(viewLifecycleOwner, ::onStatsChanged)
|
||||||
|
viewModel.startDate.observe(viewLifecycleOwner) {
|
||||||
|
binding.textViewStart.textAndVisible = it?.format(resources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onStatsChanged(stats: IntList) {
|
||||||
|
val chartView = viewBinding?.chartView ?: return
|
||||||
|
if (stats.isEmpty()) {
|
||||||
|
chartView.setData(emptyList())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val bars = ArrayList<BarChartView.Bar>(stats.size)
|
||||||
|
stats.forEach { pages ->
|
||||||
|
bars.add(
|
||||||
|
BarChartView.Bar(
|
||||||
|
value = pages,
|
||||||
|
label = pages.toString(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
chartView.setData(bars)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val ARG_MANGA = "manga"
|
||||||
|
|
||||||
|
private const val TAG = "MangaStatsSheet"
|
||||||
|
|
||||||
|
fun show(fm: FragmentManager, manga: Manga) {
|
||||||
|
MangaStatsSheet().withArgs(1) {
|
||||||
|
putParcelable(ARG_MANGA, ParcelableManga(manga))
|
||||||
|
}.showDistinct(fm, TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package org.koitharu.kotatsu.stats.ui.sheet
|
||||||
|
|
||||||
|
import androidx.collection.IntList
|
||||||
|
import androidx.collection.LongIntMap
|
||||||
|
import androidx.collection.MutableIntList
|
||||||
|
import androidx.collection.emptyIntList
|
||||||
|
import androidx.collection.emptyLongIntMap
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.require
|
||||||
|
import org.koitharu.kotatsu.stats.data.StatsRepository
|
||||||
|
import org.koitharu.kotatsu.stats.domain.StatsRecord
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class MangaStatsViewModel @Inject constructor(
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
private val repository: StatsRepository,
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
val manga = savedStateHandle.require<ParcelableManga>(MangaStatsSheet.ARG_MANGA).manga
|
||||||
|
|
||||||
|
val stats = MutableStateFlow<IntList>(emptyIntList())
|
||||||
|
val startDate = MutableStateFlow<DateTimeAgo?>(null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
|
val timeline = repository.getMangaTimeline(manga.id)
|
||||||
|
if (timeline.isEmpty()) {
|
||||||
|
startDate.value = null
|
||||||
|
stats.value = emptyIntList()
|
||||||
|
} else {
|
||||||
|
val startDay = TimeUnit.MILLISECONDS.toDays(timeline.firstKey())
|
||||||
|
val endDay = TimeUnit.MILLISECONDS.toDays(timeline.lastKey())
|
||||||
|
val res = MutableIntList((endDay - startDay).toInt() + 1)
|
||||||
|
for (day in startDay..endDay) {
|
||||||
|
val from = TimeUnit.DAYS.toMillis(day)
|
||||||
|
val to = TimeUnit.DAYS.toMillis(day + 1)
|
||||||
|
res.add(timeline.subMap(from, true, to, false).values.sum())
|
||||||
|
}
|
||||||
|
stats.value = res
|
||||||
|
startDate.value = calculateTimeAgo(Instant.ofEpochMilli(timeline.firstKey()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package org.koitharu.kotatsu.stats.ui.views
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.DashPathEffect
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.PathEffect
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.graphics.PorterDuffXfermode
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.graphics.Xfermode
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.GestureDetector
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.collection.MutableIntList
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
|
import androidx.core.graphics.minus
|
||||||
|
import androidx.core.view.GestureDetectorCompat
|
||||||
|
import androidx.core.view.setPadding
|
||||||
|
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 kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
|
class BarChartView @JvmOverloads constructor(
|
||||||
|
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||||
|
) : View(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
private val bars = ArrayList<Bar>()
|
||||||
|
private var maxValue: Int = 0
|
||||||
|
private var spacing = context.resources.resolveDp(2f)
|
||||||
|
private val minSpace = context.resources.resolveDp(20f)
|
||||||
|
private val outlineColor = context.getThemeColor(materialR.attr.colorOutline)
|
||||||
|
private val dottedEffect = DashPathEffect(
|
||||||
|
floatArrayOf(
|
||||||
|
context.resources.resolveDp(6f),
|
||||||
|
context.resources.resolveDp(6f),
|
||||||
|
),
|
||||||
|
0f,
|
||||||
|
)
|
||||||
|
private val chartBounds = RectF()
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
var barColor: Int = Color.MAGENTA
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
paint.strokeWidth = context.resources.resolveDp(1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
if (bars.isEmpty() || chartBounds.isEmpty) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val barWidth = ((chartBounds.width() + spacing) / bars.size.toFloat() - spacing)
|
||||||
|
// dashed horizontal lines
|
||||||
|
paint.color = outlineColor
|
||||||
|
paint.style = Paint.Style.STROKE
|
||||||
|
canvas.drawLine(chartBounds.left, chartBounds.bottom, chartBounds.right, chartBounds.bottom, paint)
|
||||||
|
paint.pathEffect = dottedEffect
|
||||||
|
for (i in (0..maxValue).step(computeValueStep())) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||||
|
super.onLayout(changed, left, top, right, bottom)
|
||||||
|
invalidateBounds()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setData(value: List<Bar>) {
|
||||||
|
bars.replaceWith(value)
|
||||||
|
maxValue = if (value.isEmpty()) 0 else value.maxOf { it.value }
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computeValueStep(): Int {
|
||||||
|
val h = chartBounds.height()
|
||||||
|
var step = 1
|
||||||
|
while (h / (maxValue / step).toFloat() <= minSpace) {
|
||||||
|
step++
|
||||||
|
}
|
||||||
|
return step
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invalidateBounds() {
|
||||||
|
val inset = paint.strokeWidth
|
||||||
|
chartBounds.set(
|
||||||
|
paddingLeft.toFloat() + inset,
|
||||||
|
paddingTop.toFloat() + inset,
|
||||||
|
(width - paddingLeft - paddingRight).toFloat() - inset,
|
||||||
|
(height - paddingTop - paddingBottom).toFloat() - inset,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Bar(
|
||||||
|
val value: Int,
|
||||||
|
val label: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingHorizontal="@dimen/margin_small"
|
|
||||||
android:paddingBottom="@dimen/margin_normal">
|
|
||||||
|
|
||||||
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
|
|
||||||
android:id="@+id/dragHandle"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_alignParentEnd="true" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textView_title"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignWithParentIfMissing="true"
|
|
||||||
android:layout_below="@id/dragHandle"
|
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:layout_toStartOf="@id/textView_label"
|
|
||||||
android:paddingHorizontal="@dimen/margin_small"
|
|
||||||
android:paddingBottom="@dimen/margin_small"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:text="@string/grid_size"
|
|
||||||
android:textAppearance="?textAppearanceTitleMedium" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textView_label"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignBaseline="@id/textView_title"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:paddingHorizontal="@dimen/margin_small"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textAppearance="?textAppearanceLabelLarge"
|
|
||||||
tools:text="100%" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/button_small"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_alignTop="@id/slider_grid"
|
|
||||||
android:layout_alignBottom="@id/slider_grid"
|
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:background="?selectableItemBackgroundBorderless"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:src="@drawable/ic_size_small"
|
|
||||||
android:theme="@style/ThemeOverlay.Kotatsu.MainToolbar" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/button_large"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_alignTop="@id/slider_grid"
|
|
||||||
android:layout_alignBottom="@id/slider_grid"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:background="?selectableItemBackgroundBorderless"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:src="@drawable/ic_size_large"
|
|
||||||
android:theme="@style/ThemeOverlay.Kotatsu.MainToolbar" />
|
|
||||||
|
|
||||||
<com.google.android.material.slider.Slider
|
|
||||||
android:id="@+id/slider_grid"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@id/textView_title"
|
|
||||||
android:layout_toStartOf="@id/button_large"
|
|
||||||
android:layout_toEndOf="@id/button_small"
|
|
||||||
android:stepSize="5"
|
|
||||||
android:valueFrom="50"
|
|
||||||
android:valueTo="150"
|
|
||||||
app:labelBehavior="gone"
|
|
||||||
app:tickVisible="false"
|
|
||||||
tools:value="100" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
41
app/src/main/res/layout/sheet_stats_manga.xml
Normal file
41
app/src/main/res/layout/sheet_stats_manga.xml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="@dimen/screen_padding">
|
||||||
|
|
||||||
|
<org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetHeaderBar
|
||||||
|
android:id="@+id/headerBar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:title="@string/reading_stats" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="@dimen/screen_padding"
|
||||||
|
android:textAppearance="?textAppearanceTitleMedium"
|
||||||
|
tools:text="@tools:sample/lorem[4]" />
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
Reference in New Issue
Block a user