Stats activity
This commit is contained in:
@@ -239,6 +239,9 @@
|
|||||||
<data android:scheme="kotatsu+kitsu" />
|
<data android:scheme="kotatsu+kitsu" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.stats.ui.StatsActivity"
|
||||||
|
android:label="@string/reading_stats" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ data class ReadingTime(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun format(resources: Resources): String = when {
|
fun format(resources: Resources): String = when {
|
||||||
|
hours == 0 && minutes == 0 -> resources.getString(R.string.less_than_minute)
|
||||||
hours == 0 -> resources.getQuantityString(R.plurals.minutes, minutes, minutes)
|
hours == 0 -> resources.getQuantityString(R.plurals.minutes, minutes, minutes)
|
||||||
minutes == 0 -> resources.getQuantityString(R.plurals.hours, hours, hours)
|
minutes == 0 -> resources.getQuantityString(R.plurals.hours, hours, hours)
|
||||||
else -> resources.getString(
|
else -> resources.getString(
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import org.koitharu.kotatsu.R
|
|||||||
import org.koitharu.kotatsu.core.os.NetworkManageIntent
|
import org.koitharu.kotatsu.core.os.NetworkManageIntent
|
||||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||||
import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper
|
import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper
|
||||||
|
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
|
||||||
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||||
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
|
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
|
||||||
@@ -27,6 +29,7 @@ class HistoryListFragment : MangaListFragment() {
|
|||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
RecyclerScrollKeeper(binding.recyclerView).attach()
|
RecyclerScrollKeeper(binding.recyclerView).attach()
|
||||||
addMenuProvider(HistoryListMenuProvider(binding.root.context, viewModel))
|
addMenuProvider(HistoryListMenuProvider(binding.root.context, viewModel))
|
||||||
|
viewModel.isStatsEnabled.observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScrolledToEnd() = Unit
|
override fun onScrolledToEnd() = Unit
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.history.ui
|
package org.koitharu.kotatsu.history.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
@@ -9,6 +10,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.ui.dialog.RememberSelectionDialogListener
|
import org.koitharu.kotatsu.core.ui.dialog.RememberSelectionDialogListener
|
||||||
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
|
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
|
||||||
|
import org.koitharu.kotatsu.stats.ui.StatsActivity
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
@@ -24,6 +26,11 @@ class HistoryListMenuProvider(
|
|||||||
menuInflater.inflate(R.menu.opt_history, menu)
|
menuInflater.inflate(R.menu.opt_history, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPrepareMenu(menu: Menu) {
|
||||||
|
super.onPrepareMenu(menu)
|
||||||
|
menu.findItem(R.id.action_stats)?.isVisible = viewModel.isStatsEnabled.value
|
||||||
|
}
|
||||||
|
|
||||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||||
return when (menuItem.itemId) {
|
return when (menuItem.itemId) {
|
||||||
R.id.action_clear_history -> {
|
R.id.action_clear_history -> {
|
||||||
@@ -31,6 +38,11 @@ class HistoryListMenuProvider(
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.action_stats -> {
|
||||||
|
context.startActivity(Intent(context, StatsActivity::class.java))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
g && s.isGroupingSupported()
|
g && s.isGroupingSupported()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isStatsEnabled = settings.observeAsStateFlow(
|
||||||
|
scope = viewModelScope + Dispatchers.Default,
|
||||||
|
key = AppSettings.KEY_STATS_ENABLED,
|
||||||
|
valueProducer = { isStatsEnabled },
|
||||||
|
)
|
||||||
|
|
||||||
override val content = combine(
|
override val content = combine(
|
||||||
sortOrder.flatMapLatest { repository.observeAllWithHistory(it) },
|
sortOrder.flatMapLatest { repository.observeAllWithHistory(it) },
|
||||||
isGroupingEnabled,
|
isGroupingEnabled,
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.settings
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class StatsSettingsFragment : BasePreferenceFragment(R.string.reading_stats) {
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
addPreferencesFromResource(R.xml.pref_stats)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,7 @@ interface StatsDao {
|
|||||||
@Query("SELECT IFNULL(SUM(duration), 0) FROM stats")
|
@Query("SELECT IFNULL(SUM(duration), 0) FROM stats")
|
||||||
suspend fun getTotalReadingTime(): Long
|
suspend fun getTotalReadingTime(): Long
|
||||||
|
|
||||||
@Query("SELECT manga_id, SUM(duration) AS d FROM stats GROUP BY manga_id ORDER BY d")
|
@Query("SELECT manga_id, SUM(duration) AS d FROM stats GROUP BY manga_id ORDER BY d DESC")
|
||||||
suspend fun getDurationStats(): Map<@MapColumn("manga_id") Long, @MapColumn("d") Long>
|
suspend fun getDurationStats(): Map<@MapColumn("manga_id") Long, @MapColumn("d") Long>
|
||||||
|
|
||||||
@Upsert
|
@Upsert
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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.StatsRecord
|
import org.koitharu.kotatsu.stats.domain.StatsRecord
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class StatsRepository @Inject constructor(
|
class StatsRepository @Inject constructor(
|
||||||
@@ -12,14 +13,23 @@ class StatsRepository @Inject constructor(
|
|||||||
|
|
||||||
suspend fun getReadingStats(): List<StatsRecord> = db.withTransaction {
|
suspend fun getReadingStats(): List<StatsRecord> = db.withTransaction {
|
||||||
val stats = db.getStatsDao().getDurationStats()
|
val stats = db.getStatsDao().getDurationStats()
|
||||||
|
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)
|
||||||
for ((mangaId, duration) in stats) {
|
for ((mangaId, duration) in stats) {
|
||||||
val manga = mangaDao.find(mangaId)?.toManga() ?: continue
|
val manga = mangaDao.find(mangaId)?.toManga()
|
||||||
result += StatsRecord(
|
if (manga == null || duration < minute) {
|
||||||
manga = manga,
|
other = other.copy(duration = other.duration + duration)
|
||||||
duration = duration,
|
} else {
|
||||||
)
|
result += StatsRecord(
|
||||||
|
manga = manga,
|
||||||
|
duration = duration,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (other.duration != 0L) {
|
||||||
|
result += other
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.stats.domain
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.ColorUtils
|
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
|
||||||
@@ -13,7 +14,7 @@ import java.util.concurrent.TimeUnit
|
|||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
data class StatsRecord(
|
data class StatsRecord(
|
||||||
val manga: Manga,
|
val manga: Manga?,
|
||||||
val duration: Long,
|
val duration: Long,
|
||||||
) : ListModel {
|
) : ListModel {
|
||||||
|
|
||||||
@@ -34,8 +35,12 @@ data class StatsRecord(
|
|||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
fun getColor(context: Context): Int {
|
fun getColor(context: Context): Int {
|
||||||
val hue = (manga.id.absoluteValue % 360).toFloat()
|
val color = if (manga != null) {
|
||||||
val color = ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))
|
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)
|
val backgroundColor = context.getThemeColor(R.attr.colorSurfaceContainerHigh)
|
||||||
return MaterialColors.harmonize(color, backgroundColor)
|
return MaterialColors.harmonize(color, backgroundColor)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.stats.ui
|
|||||||
|
|
||||||
import android.content.res.ColorStateList
|
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.core.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
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
|
||||||
@@ -14,12 +15,13 @@ fun statsAD(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
binding.root.setOnClickListener { v ->
|
binding.root.setOnClickListener { v ->
|
||||||
listener.onItemClick(item.manga, v)
|
listener.onItemClick(item.manga ?: return@setOnClickListener, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
bind {
|
bind {
|
||||||
binding.textViewTitle.text = item.manga.title
|
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(item.getColor(context))
|
||||||
|
binding.root.isClickable = item.manga != null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.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.util.ext.observe
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
import org.koitharu.kotatsu.databinding.FragmentStatsBinding
|
import org.koitharu.kotatsu.databinding.ActivityStatsBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
@@ -19,28 +22,26 @@ import org.koitharu.kotatsu.stats.domain.StatsRecord
|
|||||||
import org.koitharu.kotatsu.stats.ui.views.PieChartView
|
import org.koitharu.kotatsu.stats.ui.views.PieChartView
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class StatsFragment : BaseFragment<FragmentStatsBinding>(), OnListItemClickListener<Manga> {
|
class StatsActivity : BaseActivity<ActivityStatsBinding>(), OnListItemClickListener<Manga> {
|
||||||
|
|
||||||
private val viewModel: StatsViewModel by viewModels()
|
private val viewModel: StatsViewModel by viewModels()
|
||||||
|
|
||||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentStatsBinding {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
return FragmentStatsBinding.inflate(inflater, container, false)
|
super.onCreate(savedInstanceState)
|
||||||
}
|
setContentView(ActivityStatsBinding.inflate(layoutInflater))
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
override fun onViewBindingCreated(binding: FragmentStatsBinding, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
|
||||||
val adapter = BaseListAdapter<StatsRecord>()
|
val adapter = BaseListAdapter<StatsRecord>()
|
||||||
.addDelegate(ListItemType.FEED, statsAD(this))
|
.addDelegate(ListItemType.FEED, statsAD(this))
|
||||||
binding.recyclerView.adapter = adapter
|
viewBinding.recyclerView.adapter = adapter
|
||||||
viewModel.readingStats.observe(viewLifecycleOwner) {
|
viewModel.readingStats.observe(this) {
|
||||||
val sum = it.sumOf { it.duration }
|
val sum = it.sumOf { it.duration }
|
||||||
binding.chart.setData(
|
viewBinding.chart.setData(
|
||||||
it.map { v ->
|
it.map { v ->
|
||||||
PieChartView.Segment(
|
PieChartView.Segment(
|
||||||
value = (v.duration / 1000).toInt(),
|
value = (v.duration / 1000).toInt(),
|
||||||
label = v.manga.title,
|
label = v.manga?.title ?: getString(R.string.other_manga),
|
||||||
percent = (v.duration.toDouble() / sum).toFloat(),
|
percent = (v.duration.toDouble() / sum).toFloat(),
|
||||||
color = v.getColor(binding.chart.context),
|
color = v.getColor(this),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.stats.ui.views
|
package org.koitharu.kotatsu.stats.ui.views
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
@@ -9,28 +10,37 @@ import android.graphics.PorterDuffXfermode
|
|||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import android.graphics.Xfermode
|
import android.graphics.Xfermode
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.view.GestureDetector
|
||||||
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.collection.MutableIntList
|
import androidx.collection.MutableIntList
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.graphics.minus
|
import androidx.core.graphics.minus
|
||||||
|
import androidx.core.view.GestureDetectorCompat
|
||||||
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.core.util.ext.resolveDp
|
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||||
import org.koitharu.kotatsu.parsers.util.replaceWith
|
import org.koitharu.kotatsu.parsers.util.replaceWith
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.sqrt
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
class PieChartView @JvmOverloads constructor(
|
class PieChartView @JvmOverloads constructor(
|
||||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||||
) : View(context, attrs, defStyleAttr) {
|
) : View(context, attrs, defStyleAttr), GestureDetector.OnGestureListener {
|
||||||
|
|
||||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
private val segments = ArrayList<Segment>()
|
private val segments = ArrayList<Segment>()
|
||||||
private val chartBounds = RectF()
|
private val chartBounds = RectF()
|
||||||
private val clearColor = context.getThemeColor(android.R.attr.colorBackground)
|
private val clearColor = context.getThemeColor(android.R.attr.colorBackground)
|
||||||
|
private val touchDetector = GestureDetectorCompat(context, this)
|
||||||
|
private var hightlightedSegment = -1
|
||||||
|
|
||||||
|
var onSegmentClickListener: OnSegmentClickListener? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
touchDetector.setIsLongpressEnabled(false)
|
||||||
paint.strokeWidth = context.resources.resolveDp(2f)
|
paint.strokeWidth = context.resources.resolveDp(2f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +49,9 @@ class PieChartView @JvmOverloads constructor(
|
|||||||
var angle = 0f
|
var angle = 0f
|
||||||
for ((i, segment) in segments.withIndex()) {
|
for ((i, segment) in segments.withIndex()) {
|
||||||
paint.color = segment.color
|
paint.color = segment.color
|
||||||
|
if (i == hightlightedSegment) {
|
||||||
|
paint.color = ColorUtils.setAlphaComponent(paint.color, 180)
|
||||||
|
}
|
||||||
paint.style = Paint.Style.FILL
|
paint.style = Paint.Style.FILL
|
||||||
val sweepAngle = segment.percent * 360f
|
val sweepAngle = segment.percent * 360f
|
||||||
canvas.drawArc(
|
canvas.drawArc(
|
||||||
@@ -75,15 +88,80 @@ class PieChartView @JvmOverloads constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
if (event.actionMasked == MotionEvent.ACTION_CANCEL || event.actionMasked == MotionEvent.ACTION_UP) {
|
||||||
|
hightlightedSegment = -1
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
return super.onTouchEvent(event) || touchDetector.onTouchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDown(e: MotionEvent): Boolean {
|
||||||
|
val segment = findSegmentIndex(e.x, e.y)
|
||||||
|
if (segment != hightlightedSegment) {
|
||||||
|
hightlightedSegment = segment
|
||||||
|
invalidate()
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShowPress(e: MotionEvent) = Unit
|
||||||
|
|
||||||
|
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||||
|
onSegmentClickListener?.run {
|
||||||
|
val segment = segments.getOrNull(findSegmentIndex(e.x, e.y))
|
||||||
|
if (segment != null) {
|
||||||
|
onSegmentClick(this@PieChartView, segment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean = false
|
||||||
|
|
||||||
|
override fun onLongPress(e: MotionEvent) = Unit
|
||||||
|
|
||||||
|
override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean = false
|
||||||
|
|
||||||
fun setData(value: List<Segment>) {
|
fun setData(value: List<Segment>) {
|
||||||
segments.replaceWith(value)
|
segments.replaceWith(value)
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun findSegmentIndex(x: Float, y: Float): Int {
|
||||||
|
val dy = (y - chartBounds.centerY()).toDouble()
|
||||||
|
val dx = (x - chartBounds.centerX()).toDouble()
|
||||||
|
val distance = sqrt(dx * dx + dy * dy).toFloat()
|
||||||
|
if (distance < chartBounds.height() / 4f || distance > chartBounds.centerX()) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
var touchAngle = Math.toDegrees(Math.atan2(dy, dx)).toFloat()
|
||||||
|
if (touchAngle < 0) {
|
||||||
|
touchAngle += 360
|
||||||
|
}
|
||||||
|
var angle = 0f
|
||||||
|
for ((i, segment) in segments.withIndex()) {
|
||||||
|
val sweepAngle = segment.percent * 360f
|
||||||
|
if (touchAngle in angle..(angle + sweepAngle)) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
angle += sweepAngle
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
class Segment(
|
class Segment(
|
||||||
val value: Int,
|
val value: Int,
|
||||||
val label: String,
|
val label: String,
|
||||||
val percent: Float,
|
val percent: Float,
|
||||||
val color: Int,
|
val color: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
interface OnSegmentClickListener {
|
||||||
|
|
||||||
|
fun onSegmentClick(view: PieChartView, segment: Segment)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
app/src/main/res/layout/activity_stats.xml
Normal file
54
app/src/main/res/layout/activity_stats.xml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
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">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:layout_scrollFlags="noScroll">
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.MaterialToolbar>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<org.koitharu.kotatsu.stats.ui.views.PieChartView
|
||||||
|
android:id="@+id/chart"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_margin="24dp"
|
||||||
|
app:layout_constraintDimensionRatio="1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/appbar" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:overScrollMode="ifContentScrolls"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/chart"
|
||||||
|
tools:itemCount="4"
|
||||||
|
tools:listitem="@layout/item_stats" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<ScrollView
|
|
||||||
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:background="?android:colorBackground"
|
|
||||||
android:fillViewport="true">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<org.koitharu.kotatsu.stats.ui.views.PieChartView
|
|
||||||
android:id="@+id/chart"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_margin="24dp"
|
|
||||||
app:layout_constraintDimensionRatio="1"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recyclerView"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_marginTop="24dp"
|
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/chart"
|
|
||||||
tools:itemCount="4"
|
|
||||||
tools:listitem="@layout/item_stats" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</ScrollView>
|
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
android:id="@+id/imageView_badge"
|
android:id="@+id/imageView_badge"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@null"
|
||||||
app:srcCompat="@drawable/bg_rounded_square" />
|
app:srcCompat="@drawable/bg_rounded_square" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -9,4 +9,10 @@
|
|||||||
android:title="@string/clear_history"
|
android:title="@string/clear_history"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_stats"
|
||||||
|
android:orderInCategory="40"
|
||||||
|
android:title="@string/statistics"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -605,4 +605,7 @@
|
|||||||
<string name="multiple_cbz_files">Multiple CBZ files</string>
|
<string name="multiple_cbz_files">Multiple CBZ files</string>
|
||||||
<string name="stats_enabled">Enable statistics</string>
|
<string name="stats_enabled">Enable statistics</string>
|
||||||
<string name="reading_stats">Reading statistics</string>
|
<string name="reading_stats">Reading statistics</string>
|
||||||
|
<string name="other_manga">Other manga</string>
|
||||||
|
<string name="less_than_minute">Less than a minute</string>
|
||||||
|
<string name="statistics">Statistics</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -28,9 +28,9 @@
|
|||||||
android:summary="@string/related_manga_summary"
|
android:summary="@string/related_manga_summary"
|
||||||
android:title="@string/related_manga" />
|
android:title="@string/related_manga" />
|
||||||
|
|
||||||
<Preference
|
<SwitchPreferenceCompat
|
||||||
android:fragment="org.koitharu.kotatsu.settings.StatsSettingsFragment"
|
android:defaultValue="false"
|
||||||
android:key="stats"
|
android:key="stats_on"
|
||||||
android:title="@string/reading_stats"
|
android:title="@string/reading_stats"
|
||||||
app:allowDividerAbove="true" />
|
app:allowDividerAbove="true" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.preference.PreferenceScreen
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:key="stats_on"
|
|
||||||
android:layout="@layout/preference_toggle_header"
|
|
||||||
android:title="@string/stats_enabled" />
|
|
||||||
|
|
||||||
<Preference
|
|
||||||
android:dependency="stats_on"
|
|
||||||
android:fragment="org.koitharu.kotatsu.stats.ui.StatsFragment"
|
|
||||||
android:title="@string/reading_stats" />
|
|
||||||
|
|
||||||
</androidx.preference.PreferenceScreen>
|
|
||||||
Reference in New Issue
Block a user