Improve stats ui

This commit is contained in:
Koitharu
2024-02-29 12:01:09 +02:00
parent 20461112d2
commit fda59996aa
7 changed files with 116 additions and 53 deletions

View File

@@ -4,34 +4,34 @@ import androidx.room.Dao
import androidx.room.MapColumn
import androidx.room.Query
import androidx.room.Upsert
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
import org.koitharu.kotatsu.parsers.model.Manga
@Dao
abstract class StatsDao {
interface StatsDao {
@Query("SELECT * FROM stats ORDER BY started_at")
abstract suspend fun findAll(): List<StatsEntity>
suspend fun findAll(): List<StatsEntity>
@Query("SELECT * FROM stats WHERE manga_id = :mangaId ORDER BY started_at")
abstract suspend fun findAll(mangaId: Long): List<StatsEntity>
suspend fun findAll(mangaId: Long): List<StatsEntity>
@Query("SELECT IFNULL(SUM(pages),0) FROM stats WHERE manga_id = :mangaId")
abstract suspend fun getReadPagesCount(mangaId: Long): Int
suspend fun getReadPagesCount(mangaId: Long): Int
@Query("SELECT IFNULL(SUM(duration)/SUM(pages), 0) FROM stats WHERE manga_id = :mangaId")
abstract suspend fun getAverageTimePerPage(mangaId: Long): Long
suspend fun getAverageTimePerPage(mangaId: Long): Long
@Query("SELECT IFNULL(SUM(duration)/SUM(pages), 0) FROM stats")
suspend fun getAverageTimePerPage(): Long
@Query("SELECT IFNULL(SUM(duration), 0) FROM stats WHERE manga_id = :mangaId")
abstract suspend fun getReadingTime(mangaId: Long): Long
suspend fun getReadingTime(mangaId: Long): Long
@Query("SELECT IFNULL(SUM(duration), 0) FROM stats")
abstract 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")
abstract suspend fun getDurationStats(): Map<@MapColumn("manga_id") Long, @MapColumn("d") Long>
suspend fun getDurationStats(): Map<@MapColumn("manga_id") Long, @MapColumn("d") Long>
@Upsert
abstract suspend fun upsert(entity: StatsEntity)
suspend fun upsert(entity: StatsEntity)
}

View File

@@ -1,14 +1,9 @@
package org.koitharu.kotatsu.stats.data
import androidx.collection.ArrayMap
import androidx.collection.MutableScatterMap
import androidx.collection.ScatterMap
import androidx.room.withTransaction
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.stats.domain.StatsRecord
import java.util.Date
import javax.inject.Inject
class StatsRepository @Inject constructor(

View File

@@ -7,16 +7,19 @@ import com.google.android.material.R
import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.details.data.ReadingTime
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue
import kotlin.math.min
data class StatsRecord(
val manga: Manga,
val duration: Long,
) {
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is StatsRecord && other.manga == manga
}
val time: ReadingTime

View File

@@ -0,0 +1,25 @@
package org.koitharu.kotatsu.stats.ui
import android.content.res.ColorStateList
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemStatsBinding
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.stats.domain.StatsRecord
fun statsAD(
listener: OnListItemClickListener<Manga>,
) = adapterDelegateViewBinding<StatsRecord, StatsRecord, ItemStatsBinding>(
{ layoutInflater, parent -> ItemStatsBinding.inflate(layoutInflater, parent, false) },
) {
binding.root.setOnClickListener { v ->
listener.onItemClick(item.manga, v)
}
bind {
binding.textViewTitle.text = item.manga.title
binding.textViewSummary.text = item.time.format(context.resources)
binding.imageViewBadge.imageTintList = ColorStateList.valueOf(item.getColor(context))
}
}

View File

@@ -1,29 +1,25 @@
package org.koitharu.kotatsu.stats.ui
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.graphics.drawable.shapes.Shape
import android.os.Bundle
import android.text.style.DynamicDrawableSpan
import android.text.style.ImageSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
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.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.stats.domain.StatsRecord
import org.koitharu.kotatsu.stats.ui.views.PieChartView
@AndroidEntryPoint
class StatsFragment : BaseFragment<FragmentStatsBinding>() {
class StatsFragment : BaseFragment<FragmentStatsBinding>(), OnListItemClickListener<Manga> {
private val viewModel: StatsViewModel by viewModels()
@@ -33,6 +29,9 @@ class StatsFragment : BaseFragment<FragmentStatsBinding>() {
override fun onViewBindingCreated(binding: FragmentStatsBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val adapter = BaseListAdapter<StatsRecord>()
.addDelegate(ListItemType.FEED, statsAD(this))
binding.recyclerView.adapter = adapter
viewModel.readingStats.observe(viewLifecycleOwner) {
val sum = it.sumOf { it.duration }
binding.chart.setData(
@@ -45,27 +44,13 @@ class StatsFragment : BaseFragment<FragmentStatsBinding>() {
)
},
)
binding.textViewLegend.text = buildLegend(it)
adapter.emit(it)
}
}
override fun onWindowInsetsChanged(insets: Insets) = Unit
private fun buildLegend(stats: List<StatsRecord>) = buildSpannedString {
val context = context ?: return@buildSpannedString
for (item in stats) {
ContextCompat.getDrawable(context, R.drawable.bg_rounded_square)?.let { icon ->
icon.setBounds(0, 0, icon.intrinsicWidth, icon.intrinsicHeight)
icon.setTint(item.getColor(context))
inSpans(ImageSpan(icon, DynamicDrawableSpan.ALIGN_BASELINE)) {
append(' ')
}
append(' ')
}
append(item.manga.title)
append(" - ")
append(item.time.format(context.resources))
appendLine()
}
override fun onItemClick(item: Manga, view: View) {
startActivity(DetailsActivity.newIntent(view.context, item))
}
}

View File

@@ -4,7 +4,9 @@
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:layout_height="match_parent"
android:background="?android:colorBackground"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@@ -21,16 +23,18 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView_legend"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:layout_marginTop="24dp"
android:paddingHorizontal="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:text="@tools:sample/lorem/random" />
tools:itemCount="4"
tools:listitem="@layout/item_stats" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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="wrap_content"
android:background="@drawable/list_selector"
android:clipChildren="false"
android:gravity="center_vertical"
android:minHeight="?listPreferredItemHeightSmall"
android:orientation="horizontal"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd">
<ImageView
android:id="@+id/imageView_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/bg_rounded_square" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceTitleSmall"
tools:text="@tools:sample/lorem[3]" />
<TextView
android:id="@+id/textView_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
</LinearLayout>