Statistics periods

This commit is contained in:
Koitharu
2024-02-29 15:28:57 +02:00
parent 096f5b15dc
commit 4694215ccc
8 changed files with 89 additions and 7 deletions

View File

@@ -68,6 +68,13 @@ abstract class BaseViewModel : ViewModel() {
errorEvent.call(error)
}
protected inline suspend fun <T> withLoading(block: () -> T): T = try {
loadingCounter.increment()
block()
} finally {
loadingCounter.decrement()
}
protected fun MutableStateFlow<Int>.increment() = update { it + 1 }
protected fun MutableStateFlow<Int>.decrement() = update { it - 1 }

View File

@@ -29,8 +29,8 @@ 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 DESC")
suspend fun getDurationStats(): Map<@MapColumn("manga_id") Long, @MapColumn("d") Long>
@Query("SELECT manga_id, SUM(duration) AS d FROM stats WHERE started_at >= :fromDate GROUP BY manga_id ORDER BY d DESC")
suspend fun getDurationStats(fromDate: Long): Map<@MapColumn("manga_id") Long, @MapColumn("d") Long>
@Query("DELETE FROM stats")
suspend fun clear()

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.stats.data
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.StatsPeriod
import org.koitharu.kotatsu.stats.domain.StatsRecord
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -11,8 +12,13 @@ class StatsRepository @Inject constructor(
private val db: MangaDatabase,
) {
suspend fun getReadingStats(): List<StatsRecord> = db.withTransaction {
val stats = db.getStatsDao().getDurationStats()
suspend fun getReadingStats(period: StatsPeriod): List<StatsRecord> = db.withTransaction {
val fromDate = if (period == StatsPeriod.ALL) {
0L
} else {
System.currentTimeMillis() - TimeUnit.DAYS.toMillis(period.days.toLong())
}
val stats = db.getStatsDao().getDurationStats(fromDate)
val minute = TimeUnit.MINUTES.toMillis(1)
val mangaDao = db.getMangaDao()
val result = ArrayList<StatsRecord>(stats.size)

View File

@@ -0,0 +1,16 @@
package org.koitharu.kotatsu.stats.domain
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
enum class StatsPeriod(
@StringRes val titleResId: Int,
val days: Int,
) {
DAY(R.string.day, 1),
WEEK(R.string.week, 7),
MONTH(R.string.month, 30),
MONTHS_3(R.string.three_months, 90),
ALL(R.string.all_time, Int.MAX_VALUE),
}

View File

@@ -1,6 +1,11 @@
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
@@ -9,7 +14,10 @@ import android.view.View
import android.view.ViewGroup
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.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
@@ -20,6 +28,7 @@ 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.ext.DIALOG_THEME_CENTERED
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.showOrHide
@@ -27,6 +36,7 @@ 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
import org.koitharu.kotatsu.stats.domain.StatsPeriod
import org.koitharu.kotatsu.stats.domain.StatsRecord
import org.koitharu.kotatsu.stats.ui.views.PieChartView
@@ -47,6 +57,9 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(), OnListItemClickListe
viewModel.isLoading.observe(this) {
viewBinding.progressBar.showOrHide(it)
}
viewModel.period.observe(this) {
supportActionBar?.setSubtitle(it.titleResId)
}
viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.recyclerView))
viewModel.readingStats.observe(this) {
val sum = it.sumOf { it.duration }
@@ -88,6 +101,11 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(), OnListItemClickListe
true
}
R.id.action_period -> {
showPeriodSelector()
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -102,4 +120,17 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(), OnListItemClickListe
viewModel.clear()
}.show()
}
private fun showPeriodSelector() {
val menu = PopupMenu(this, viewBinding.toolbar)
for ((i, branch) in StatsPeriod.entries.withIndex()) {
menu.menu.add(Menu.NONE, Menu.NONE, i, branch.titleResId)
}
menu.setOnMenuItemClickListener {
StatsPeriod.entries.getOrNull(it.order)?.also {
viewModel.period.value = it
} != null
}
menu.show()
}
}

View File

@@ -5,15 +5,18 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.stats.data.StatsRepository
import org.koitharu.kotatsu.stats.domain.StatsPeriod
import org.koitharu.kotatsu.stats.domain.StatsRecord
import javax.inject.Inject
@@ -22,12 +25,17 @@ class StatsViewModel @Inject constructor(
private val repository: StatsRepository,
) : BaseViewModel() {
val period = MutableStateFlow(StatsPeriod.WEEK)
val onActionDone = MutableEventFlow<ReversibleAction>()
val readingStats = MutableStateFlow<List<StatsRecord>>(emptyList())
init {
launchLoadingJob(Dispatchers.Default) {
readingStats.value = repository.getReadingStats()
launchJob(Dispatchers.Default) {
period.collectLatest { p ->
readingStats.value = withLoading {
repository.getReadingStats(p)
}
}
}
}

View File

@@ -1,7 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/action_period"
android:icon="@drawable/ic_expand_more"
android:orderInCategory="0"
android:title="@string/chapters"
app:showAsAction="always"
tools:ignore="AlwaysShowAction" />
<item
android:id="@+id/action_clear"

View File

@@ -611,4 +611,9 @@
<string name="clear_stats">Clear statistics</string>
<string name="stats_cleared">Statistics cleared</string>
<string name="clear_stats_confirm">Do you really want to clear all reading statistics? This action cannot be undone.</string>
<string name="week">Week</string>
<string name="month">Month</string>
<string name="all_time">All time</string>
<string name="day">Day</string>
<string name="three_months">Three months</string>
</resources>