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.MapColumn
import androidx.room.Query import androidx.room.Query
import androidx.room.Upsert 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 @Dao
abstract class StatsDao { interface StatsDao {
@Query("SELECT * FROM stats ORDER BY started_at") @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") @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") @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") @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") @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") @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") @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 @Upsert
abstract suspend fun upsert(entity: StatsEntity) suspend fun upsert(entity: StatsEntity)
} }

View File

@@ -1,14 +1,9 @@
package org.koitharu.kotatsu.stats.data package org.koitharu.kotatsu.stats.data
import androidx.collection.ArrayMap
import androidx.collection.MutableScatterMap
import androidx.collection.ScatterMap
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.parsers.model.Manga
import org.koitharu.kotatsu.stats.domain.StatsRecord import org.koitharu.kotatsu.stats.domain.StatsRecord
import java.util.Date
import javax.inject.Inject import javax.inject.Inject
class StatsRepository @Inject constructor( class StatsRepository @Inject constructor(

View File

@@ -7,16 +7,19 @@ import com.google.android.material.R
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.details.data.ReadingTime import org.koitharu.kotatsu.details.data.ReadingTime
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.min
data class StatsRecord( data class StatsRecord(
val manga: Manga, val manga: Manga,
val duration: Long, val duration: Long,
) { ) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is StatsRecord && other.manga == manga
}
val time: ReadingTime 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 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.os.Bundle
import android.text.style.DynamicDrawableSpan
import android.text.style.ImageSpan
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat
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 dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseFragment 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.core.util.ext.observe
import org.koitharu.kotatsu.databinding.FragmentStatsBinding 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.parsers.model.Manga
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
@AndroidEntryPoint @AndroidEntryPoint
class StatsFragment : BaseFragment<FragmentStatsBinding>() { class StatsFragment : BaseFragment<FragmentStatsBinding>(), OnListItemClickListener<Manga> {
private val viewModel: StatsViewModel by viewModels() private val viewModel: StatsViewModel by viewModels()
@@ -33,6 +29,9 @@ class StatsFragment : BaseFragment<FragmentStatsBinding>() {
override fun onViewBindingCreated(binding: FragmentStatsBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentStatsBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
val adapter = BaseListAdapter<StatsRecord>()
.addDelegate(ListItemType.FEED, statsAD(this))
binding.recyclerView.adapter = adapter
viewModel.readingStats.observe(viewLifecycleOwner) { viewModel.readingStats.observe(viewLifecycleOwner) {
val sum = it.sumOf { it.duration } val sum = it.sumOf { it.duration }
binding.chart.setData( 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 override fun onWindowInsetsChanged(insets: Insets) = Unit
private fun buildLegend(stats: List<StatsRecord>) = buildSpannedString { override fun onItemClick(item: Manga, view: View) {
val context = context ?: return@buildSpannedString startActivity(DetailsActivity.newIntent(view.context, item))
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()
}
} }
} }

View File

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