Statistics periods

master
Koitharu 2 years ago
parent 096f5b15dc
commit 4694215ccc
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -68,6 +68,13 @@ abstract class BaseViewModel : ViewModel() {
errorEvent.call(error) 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>.increment() = update { it + 1 }
protected fun MutableStateFlow<Int>.decrement() = update { it - 1 } protected fun MutableStateFlow<Int>.decrement() = update { it - 1 }

@ -29,8 +29,8 @@ 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 DESC") @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(): Map<@MapColumn("manga_id") Long, @MapColumn("d") Long> suspend fun getDurationStats(fromDate: Long): Map<@MapColumn("manga_id") Long, @MapColumn("d") Long>
@Query("DELETE FROM stats") @Query("DELETE FROM stats")
suspend fun clear() suspend fun clear()

@ -3,6 +3,7 @@ package org.koitharu.kotatsu.stats.data
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.StatsRecord import org.koitharu.kotatsu.stats.domain.StatsRecord
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@ -11,8 +12,13 @@ class StatsRepository @Inject constructor(
private val db: MangaDatabase, private val db: MangaDatabase,
) { ) {
suspend fun getReadingStats(): List<StatsRecord> = db.withTransaction { suspend fun getReadingStats(period: StatsPeriod): List<StatsRecord> = db.withTransaction {
val stats = db.getStatsDao().getDurationStats() 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 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)

@ -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),
}

@ -1,6 +1,11 @@
package org.koitharu.kotatsu.stats.ui package org.koitharu.kotatsu.stats.ui
import android.graphics.Color
import android.os.Bundle 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.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
@ -9,7 +14,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint 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.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver 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.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.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.showOrHide 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.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
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.views.PieChartView import org.koitharu.kotatsu.stats.ui.views.PieChartView
@ -47,6 +57,9 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(), OnListItemClickListe
viewModel.isLoading.observe(this) { viewModel.isLoading.observe(this) {
viewBinding.progressBar.showOrHide(it) viewBinding.progressBar.showOrHide(it)
} }
viewModel.period.observe(this) {
supportActionBar?.setSubtitle(it.titleResId)
}
viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.recyclerView)) viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.recyclerView))
viewModel.readingStats.observe(this) { viewModel.readingStats.observe(this) {
val sum = it.sumOf { it.duration } val sum = it.sumOf { it.duration }
@ -88,6 +101,11 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(), OnListItemClickListe
true true
} }
R.id.action_period -> {
showPeriodSelector()
true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
@ -102,4 +120,17 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(), OnListItemClickListe
viewModel.clear() viewModel.clear()
}.show() }.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()
}
} }

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

@ -1,7 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu <menu
xmlns:android="http://schemas.android.com/apk/res/android" 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 <item
android:id="@+id/action_clear" android:id="@+id/action_clear"

@ -611,4 +611,9 @@
<string name="clear_stats">Clear statistics</string> <string name="clear_stats">Clear statistics</string>
<string name="stats_cleared">Statistics cleared</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="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> </resources>

Loading…
Cancel
Save