diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseViewModel.kt index 244636ded..6a3940709 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseViewModel.kt @@ -68,6 +68,13 @@ abstract class BaseViewModel : ViewModel() { errorEvent.call(error) } + protected inline suspend fun withLoading(block: () -> T): T = try { + loadingCounter.increment() + block() + } finally { + loadingCounter.decrement() + } + protected fun MutableStateFlow.increment() = update { it + 1 } protected fun MutableStateFlow.decrement() = update { it - 1 } 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 72223485e..c9638d38d 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,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() 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 43dbffbec..696faf6f0 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 @@ -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 = db.withTransaction { - val stats = db.getStatsDao().getDurationStats() + suspend fun getReadingStats(period: StatsPeriod): List = 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(stats.size) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/domain/StatsPeriod.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/domain/StatsPeriod.kt new file mode 100644 index 000000000..24f6f96c5 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/domain/StatsPeriod.kt @@ -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), +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsActivity.kt index 5759771b2..9f1ebcc8c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsActivity.kt @@ -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(), 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(), OnListItemClickListe true } + R.id.action_period -> { + showPeriodSelector() + true + } + else -> super.onOptionsItemSelected(item) } } @@ -102,4 +120,17 @@ class StatsActivity : BaseActivity(), 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() + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsViewModel.kt index 7904d7c24..c7abf27be 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsViewModel.kt @@ -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() val readingStats = MutableStateFlow>(emptyList()) init { - launchLoadingJob(Dispatchers.Default) { - readingStats.value = repository.getReadingStats() + launchJob(Dispatchers.Default) { + period.collectLatest { p -> + readingStats.value = withLoading { + repository.getReadingStats(p) + } + } } } diff --git a/app/src/main/res/menu/opt_stats.xml b/app/src/main/res/menu/opt_stats.xml index 442858386..8fba95597 100644 --- a/app/src/main/res/menu/opt_stats.xml +++ b/app/src/main/res/menu/opt_stats.xml @@ -1,7 +1,16 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + Clear statistics Statistics cleared Do you really want to clear all reading statistics? This action cannot be undone. + Week + Month + All time + Day + Three months