diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 64de70c55..70ec0d3b7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -239,6 +239,9 @@
+
resources.getString(R.string.less_than_minute)
hours == 0 -> resources.getQuantityString(R.plurals.minutes, minutes, minutes)
minutes == 0 -> resources.getQuantityString(R.plurals.hours, hours, hours)
else -> resources.getString(
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt
index dd421f8a4..ba733b064 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt
@@ -11,7 +11,9 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkManageIntent
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
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.observe
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
@@ -27,6 +29,7 @@ class HistoryListFragment : MangaListFragment() {
super.onViewBindingCreated(binding, savedInstanceState)
RecyclerScrollKeeper(binding.recyclerView).attach()
addMenuProvider(HistoryListMenuProvider(binding.root.context, viewModel))
+ viewModel.isStatsEnabled.observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
}
override fun onScrolledToEnd() = Unit
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt
index d2a5758f3..ab1a49ccd 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt
@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.history.ui
import android.content.Context
+import android.content.Intent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
@@ -9,6 +10,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.dialog.RememberSelectionDialogListener
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
+import org.koitharu.kotatsu.stats.ui.StatsActivity
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
@@ -24,6 +26,11 @@ class HistoryListMenuProvider(
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 {
return when (menuItem.itemId) {
R.id.action_clear_history -> {
@@ -31,6 +38,11 @@ class HistoryListMenuProvider(
true
}
+ R.id.action_stats -> {
+ context.startActivity(Intent(context, StatsActivity::class.java))
+ true
+ }
+
else -> false
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt
index 91c192ac4..6a6cf249b 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt
@@ -71,6 +71,12 @@ class HistoryListViewModel @Inject constructor(
g && s.isGroupingSupported()
}
+ val isStatsEnabled = settings.observeAsStateFlow(
+ scope = viewModelScope + Dispatchers.Default,
+ key = AppSettings.KEY_STATS_ENABLED,
+ valueProducer = { isStatsEnabled },
+ )
+
override val content = combine(
sortOrder.flatMapLatest { repository.observeAllWithHistory(it) },
isGroupingEnabled,
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/StatsSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/StatsSettingsFragment.kt
deleted file mode 100644
index 3f176636d..000000000
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/StatsSettingsFragment.kt
+++ /dev/null
@@ -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)
- }
-}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsDao.kt
index 94b5ba86f..c629bbbbe 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsDao.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsDao.kt
@@ -29,7 +29,7 @@ interface StatsDao {
@Query("SELECT IFNULL(SUM(duration), 0) FROM stats")
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>
@Upsert
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 88f8f55f7..eaaa89b94 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
@@ -4,6 +4,7 @@ import androidx.room.withTransaction
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.stats.domain.StatsRecord
+import java.util.concurrent.TimeUnit
import javax.inject.Inject
class StatsRepository @Inject constructor(
@@ -12,14 +13,23 @@ class StatsRepository @Inject constructor(
suspend fun getReadingStats(): List = db.withTransaction {
val stats = db.getStatsDao().getDurationStats()
+ val minute = TimeUnit.MINUTES.toMillis(1)
val mangaDao = db.getMangaDao()
val result = ArrayList(stats.size)
+ var other = StatsRecord(null, 0)
for ((mangaId, duration) in stats) {
- val manga = mangaDao.find(mangaId)?.toManga() ?: continue
- result += StatsRecord(
- manga = manga,
- duration = duration,
- )
+ val manga = mangaDao.find(mangaId)?.toManga()
+ if (manga == null || duration < minute) {
+ other = other.copy(duration = other.duration + duration)
+ } else {
+ result += StatsRecord(
+ manga = manga,
+ duration = duration,
+ )
+ }
+ }
+ if (other.duration != 0L) {
+ result += other
}
result
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/domain/StatsRecord.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/domain/StatsRecord.kt
index 172fdc45a..30e81a9e0 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/domain/StatsRecord.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/domain/StatsRecord.kt
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.stats.domain
import android.content.Context
import androidx.annotation.ColorInt
+import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import com.google.android.material.R
import com.google.android.material.color.MaterialColors
@@ -13,7 +14,7 @@ import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue
data class StatsRecord(
- val manga: Manga,
+ val manga: Manga?,
val duration: Long,
) : ListModel {
@@ -34,8 +35,12 @@ data class StatsRecord(
@ColorInt
fun getColor(context: Context): Int {
- val hue = (manga.id.absoluteValue % 360).toFloat()
- val color = ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))
+ 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)
}
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 671b12892..d2557820b 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
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.stats.ui
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.databinding.ItemStatsBinding
import org.koitharu.kotatsu.parsers.model.Manga
@@ -14,12 +15,13 @@ fun statsAD(
) {
binding.root.setOnClickListener { v ->
- listener.onItemClick(item.manga, v)
+ listener.onItemClick(item.manga ?: return@setOnClickListener, v)
}
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.imageViewBadge.imageTintList = ColorStateList.valueOf(item.getColor(context))
+ binding.root.isClickable = item.manga != null
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsActivity.kt
similarity index 67%
rename from app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsFragment.kt
rename to app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsActivity.kt
index 34d80dc88..bd95442ef 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsActivity.kt
@@ -4,14 +4,17 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.fragment.app.viewModels
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.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.list.ui.adapter.ListItemType
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
@AndroidEntryPoint
-class StatsFragment : BaseFragment(), OnListItemClickListener {
+class StatsActivity : BaseActivity(), OnListItemClickListener {
private val viewModel: StatsViewModel by viewModels()
- override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentStatsBinding {
- return FragmentStatsBinding.inflate(inflater, container, false)
- }
-
- override fun onViewBindingCreated(binding: FragmentStatsBinding, savedInstanceState: Bundle?) {
- super.onViewBindingCreated(binding, savedInstanceState)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(ActivityStatsBinding.inflate(layoutInflater))
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
val adapter = BaseListAdapter()
.addDelegate(ListItemType.FEED, statsAD(this))
- binding.recyclerView.adapter = adapter
- viewModel.readingStats.observe(viewLifecycleOwner) {
+ viewBinding.recyclerView.adapter = adapter
+ viewModel.readingStats.observe(this) {
val sum = it.sumOf { it.duration }
- binding.chart.setData(
+ viewBinding.chart.setData(
it.map { v ->
PieChartView.Segment(
value = (v.duration / 1000).toInt(),
- label = v.manga.title,
+ label = v.manga?.title ?: getString(R.string.other_manga),
percent = (v.duration.toDouble() / sum).toFloat(),
- color = v.getColor(binding.chart.context),
+ color = v.getColor(this),
)
},
)
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 4cd1fde54..30fad194a 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
@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.stats.ui.views
+import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
@@ -9,28 +10,37 @@ 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 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.sqrt
import com.google.android.material.R as materialR
class PieChartView @JvmOverloads constructor(
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 segments = ArrayList()
private val chartBounds = RectF()
private val clearColor = context.getThemeColor(android.R.attr.colorBackground)
+ private val touchDetector = GestureDetectorCompat(context, this)
+ private var hightlightedSegment = -1
+
+ var onSegmentClickListener: OnSegmentClickListener? = null
init {
+ touchDetector.setIsLongpressEnabled(false)
paint.strokeWidth = context.resources.resolveDp(2f)
}
@@ -39,6 +49,9 @@ class PieChartView @JvmOverloads constructor(
var angle = 0f
for ((i, segment) in segments.withIndex()) {
paint.color = segment.color
+ if (i == hightlightedSegment) {
+ paint.color = ColorUtils.setAlphaComponent(paint.color, 180)
+ }
paint.style = Paint.Style.FILL
val sweepAngle = segment.percent * 360f
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) {
segments.replaceWith(value)
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(
val value: Int,
val label: String,
val percent: Float,
val color: Int,
)
+
+ interface OnSegmentClickListener {
+
+ fun onSegmentClick(view: PieChartView, segment: Segment)
+ }
}
diff --git a/app/src/main/res/layout/activity_stats.xml b/app/src/main/res/layout/activity_stats.xml
new file mode 100644
index 000000000..b33e94aa3
--- /dev/null
+++ b/app/src/main/res/layout/activity_stats.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_stats.xml b/app/src/main/res/layout/fragment_stats.xml
deleted file mode 100644
index c3c806baa..000000000
--- a/app/src/main/res/layout/fragment_stats.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/item_stats.xml b/app/src/main/res/layout/item_stats.xml
index eeb73eee4..873cf7334 100644
--- a/app/src/main/res/layout/item_stats.xml
+++ b/app/src/main/res/layout/item_stats.xml
@@ -17,6 +17,7 @@
android:id="@+id/imageView_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:contentDescription="@null"
app:srcCompat="@drawable/bg_rounded_square" />
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bd0f6d687..a2d905ab8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -605,4 +605,7 @@
Multiple CBZ files
Enable statistics
Reading statistics
+ Other manga
+ Less than a minute
+ Statistics
diff --git a/app/src/main/res/xml/pref_services.xml b/app/src/main/res/xml/pref_services.xml
index 3c7e98af4..c1d8b8bf6 100644
--- a/app/src/main/res/xml/pref_services.xml
+++ b/app/src/main/res/xml/pref_services.xml
@@ -28,9 +28,9 @@
android:summary="@string/related_manga_summary"
android:title="@string/related_manga" />
-
diff --git a/app/src/main/res/xml/pref_stats.xml b/app/src/main/res/xml/pref_stats.xml
deleted file mode 100644
index 9f164bc46..000000000
--- a/app/src/main/res/xml/pref_stats.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
-