Stats chart for single manga

This commit is contained in:
Koitharu
2024-03-04 14:42:31 +02:00
parent f7a70680bd
commit 876675445d
18 changed files with 186 additions and 88 deletions

View File

@@ -12,12 +12,10 @@ import android.graphics.RectF
import android.graphics.drawable.Drawable
import androidx.annotation.StyleRes
import androidx.core.content.withStyledAttributes
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.withClip
import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.Colors
import kotlin.math.absoluteValue
import org.koitharu.kotatsu.core.util.KotatsuColors
class FaviconDrawable(
context: Context,
@@ -45,7 +43,7 @@ class FaviconDrawable(
}
paint.textAlign = Paint.Align.CENTER
paint.isFakeBoldText = true
colorForeground = MaterialColors.harmonize(Colors.random(name), colorBackground)
colorForeground = MaterialColors.harmonize(KotatsuColors.random(name), colorBackground)
}
override fun draw(canvas: Canvas) {

View File

@@ -10,7 +10,7 @@ import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.parsers.model.Manga
import kotlin.math.absoluteValue
object Colors {
object KotatsuColors {
@ColorInt
fun segmentColor(context: Context, @AttrRes resId: Int): Int {
@@ -28,7 +28,7 @@ object Colors {
}
@ColorInt
fun of(context: Context, manga: Manga?): Int {
fun ofManga(context: Context, manga: Manga?): Int {
val color = if (manga != null) {
val hue = (manga.id.absoluteValue % 360).toFloat()
ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))

View File

@@ -138,7 +138,10 @@ class DetailsActivity :
},
),
)
viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.containerDetails, viewBinding.layoutBottom))
viewModel.onActionDone.observeEvent(
this,
ReversibleActionObserver(viewBinding.containerDetails, viewBinding.layoutBottom),
)
viewModel.onShowTip.observeEvent(this) { showTip() }
viewModel.historyInfo.observe(this, ::onHistoryChanged)
viewModel.selectedBranch.observe(this) {
@@ -150,6 +153,7 @@ class DetailsActivity :
viewModel.isChaptersEmpty.observe(this, chaptersMenuInvalidator)
val menuInvalidator = MenuInvalidator(this)
viewModel.favouriteCategories.observe(this, menuInvalidator)
viewModel.isStatsEnabled.observe(this, menuInvalidator)
viewModel.remoteManga.observe(this, menuInvalidator)
viewModel.branches.observe(this) {
viewBinding.buttonDropdown.isVisible = it.size > 1

View File

@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
class DetailsMenuProvider(
private val activity: FragmentActivity,
@@ -43,6 +44,7 @@ class DetailsMenuProvider(
menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity)
menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable
menu.findItem(R.id.action_online).isVisible = viewModel.remoteManga.value != null
menu.findItem(R.id.action_stats).isVisible = viewModel.isStatsEnabled.value
menu.findItem(R.id.action_favourite).setIcon(
if (viewModel.favouriteCategories.value) R.drawable.ic_heart else R.drawable.ic_heart_outline,
)
@@ -101,6 +103,12 @@ class DetailsMenuProvider(
}
}
R.id.action_stats -> {
viewModel.manga.value?.let {
MangaStatsSheet.show(activity.supportFragmentManager, it)
}
}
R.id.action_scrobbling -> {
viewModel.manga.value?.let {
ScrobblingSelectorSheet.show(activity.supportFragmentManager, it, null)

View File

@@ -100,6 +100,10 @@ class DetailsViewModel @Inject constructor(
val favouriteCategories = interactor.observeIsFavourite(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
val isStatsEnabled = settings.observeAsStateFlow(viewModelScope + Dispatchers.Default, AppSettings.KEY_STATS_ENABLED) {
isStatsEnabled
}
val remoteManga = MutableStateFlow<Manga?>(null)
val newChaptersCount = details.flatMapLatest { d ->

View File

@@ -2,18 +2,11 @@ package org.koitharu.kotatsu.local.ui.info
import android.content.res.ColorStateList
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.StringRes
import androidx.core.graphics.ColorUtils
import androidx.core.widget.TextViewCompat
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.combine
@@ -21,16 +14,13 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView
import org.koitharu.kotatsu.core.util.Colors
import org.koitharu.kotatsu.core.util.KotatsuColors
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.combine
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.DialogLocalInfoBinding
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.settings.userdata.StorageUsage
import com.google.android.material.R as materialR
@AndroidEntryPoint
@@ -67,7 +57,7 @@ class LocalInfoDialog : AlertDialogFragment<DialogLocalInfoBinding>() {
val total = size + available
val segment = SegmentedBarView.Segment(
percent = (size.toDouble() / total.toDouble()).toFloat(),
color = Colors.segmentColor(view.context, materialR.attr.colorPrimary),
color = KotatsuColors.segmentColor(view.context, materialR.attr.colorPrimary),
)
requireViewBinding().labelUsed.text = view.context.getString(
R.string.memory_usage_pattern,

View File

@@ -3,20 +3,15 @@ package org.koitharu.kotatsu.settings.userdata
import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.StringRes
import androidx.core.graphics.ColorUtils
import androidx.core.widget.TextViewCompat
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import com.google.android.material.color.MaterialColors
import kotlinx.coroutines.flow.FlowCollector
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView
import org.koitharu.kotatsu.core.util.Colors
import org.koitharu.kotatsu.core.util.KotatsuColors
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.databinding.PreferenceMemoryUsageBinding
import com.google.android.material.R as materialR
@@ -39,15 +34,15 @@ class StorageUsagePreference @JvmOverloads constructor(
val binding = PreferenceMemoryUsageBinding.bind(holder.itemView)
val storageSegment = SegmentedBarView.Segment(
usage?.savedManga?.percent ?: 0f,
Colors.segmentColor(context, materialR.attr.colorPrimary),
KotatsuColors.segmentColor(context, materialR.attr.colorPrimary),
)
val pagesSegment = SegmentedBarView.Segment(
usage?.pagesCache?.percent ?: 0f,
Colors.segmentColor(context, materialR.attr.colorSecondary),
KotatsuColors.segmentColor(context, materialR.attr.colorSecondary),
)
val otherSegment = SegmentedBarView.Segment(
usage?.otherCache?.percent ?: 0f,
Colors.segmentColor(context, materialR.attr.colorTertiary),
KotatsuColors.segmentColor(context, materialR.attr.colorTertiary),
)
with(binding) {

View File

@@ -56,6 +56,10 @@ class StatsRepository @Inject constructor(
time
}
suspend fun getTotalPagesRead(mangaId: Long): Int {
return db.getStatsDao().getReadPagesCount(mangaId)
}
suspend fun getMangaTimeline(mangaId: Long): NavigableMap<Long, Int> {
val entities = db.getStatsDao().findAll(mangaId)
val map = TreeMap<Long, Int>()

View File

@@ -4,7 +4,7 @@ import android.content.res.ColorStateList
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.Colors
import org.koitharu.kotatsu.core.util.KotatsuColors
import org.koitharu.kotatsu.databinding.ItemStatsBinding
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.stats.domain.StatsRecord
@@ -22,7 +22,7 @@ fun statsAD(
bind {
binding.textViewTitle.text = item.manga?.title ?: getString(R.string.other_manga)
binding.textViewSummary.text = item.time.format(context.resources)
binding.imageViewBadge.imageTintList = ColorStateList.valueOf(Colors.of(context, item.manga))
binding.imageViewBadge.imageTintList = ColorStateList.valueOf(KotatsuColors.ofManga(context, item.manga))
binding.root.isClickable = item.manga != null
}
}

View File

@@ -1,41 +1,29 @@
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
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.ViewStub
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.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.AsyncListDiffer
import coil.ImageLoader
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
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.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.Colors
import org.koitharu.kotatsu.core.util.KotatsuColors
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
@@ -43,7 +31,6 @@ import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.core.util.ext.showOrHide
import org.koitharu.kotatsu.databinding.ActivityStatsBinding
import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
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
@@ -90,7 +77,8 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(),
value = (v.duration / 1000).toInt(),
label = v.manga?.title ?: getString(R.string.other_manga),
percent = (v.duration.toDouble() / sum).toFloat(),
color = Colors.of(this, v.manga),
color = KotatsuColors.ofManga(this, v.manga),
tag = v.manga,
)
},
)
@@ -105,9 +93,8 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(),
}
override fun onSegmentClick(view: PieChartView, segment: PieChartView.Segment) {
Toast.makeText(this, segment.label, Toast.LENGTH_SHORT).apply {
setGravity(Gravity.TOP, 0, view.top + view.height / 2)
}.show()
val manga = segment.tag as? Manga ?: return
onItemClick(manga, view)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {

View File

@@ -2,27 +2,28 @@ package org.koitharu.kotatsu.stats.ui.sheet
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.collection.IntList
import androidx.collection.LongIntMap
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.bookmarks.ui.sheet.BookmarksSheet
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.Colors
import org.koitharu.kotatsu.core.util.KotatsuColors
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetStatsMangaBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.stats.ui.views.BarChartView
import java.util.concurrent.TimeUnit
@AndroidEntryPoint
class MangaStatsSheet : BaseAdaptiveSheet<SheetStatsMangaBinding>() {
class MangaStatsSheet : BaseAdaptiveSheet<SheetStatsMangaBinding>(), View.OnClickListener {
private val viewModel: MangaStatsViewModel by viewModels()
@@ -33,11 +34,19 @@ class MangaStatsSheet : BaseAdaptiveSheet<SheetStatsMangaBinding>() {
override fun onViewBindingCreated(binding: SheetStatsMangaBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
binding.textViewTitle.text = viewModel.manga.title
binding.chartView.barColor = Colors.of(binding.root.context, viewModel.manga)
binding.chartView.barColor = KotatsuColors.ofManga(binding.root.context, viewModel.manga)
viewModel.stats.observe(viewLifecycleOwner, ::onStatsChanged)
viewModel.startDate.observe(viewLifecycleOwner) {
binding.textViewStart.textAndVisible = it?.format(resources)
}
viewModel.totalPagesRead.observe(viewLifecycleOwner) {
binding.textViewPages.text = getString(R.string.pages_read_s, it.format())
}
binding.buttonOpen.setOnClickListener(this)
}
override fun onClick(v: View) {
startActivity(DetailsActivity.newIntent(v.context, viewModel.manga))
}
private fun onStatsChanged(stats: IntList) {

View File

@@ -30,6 +30,7 @@ class MangaStatsViewModel @Inject constructor(
val stats = MutableStateFlow<IntList>(emptyIntList())
val startDate = MutableStateFlow<DateTimeAgo?>(null)
val totalPagesRead = MutableStateFlow(0)
init {
launchLoadingJob(Dispatchers.Default) {
@@ -39,7 +40,7 @@ class MangaStatsViewModel @Inject constructor(
stats.value = emptyIntList()
} else {
val startDay = TimeUnit.MILLISECONDS.toDays(timeline.firstKey())
val endDay = TimeUnit.MILLISECONDS.toDays(timeline.lastKey())
val endDay = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis())
val res = MutableIntList((endDay - startDay).toInt() + 1)
for (day in startDay..endDay) {
val from = TimeUnit.DAYS.toMillis(day)
@@ -50,5 +51,8 @@ class MangaStatsViewModel @Inject constructor(
startDate.value = calculateTimeAgo(Instant.ofEpochMilli(timeline.firstKey()))
}
}
launchLoadingJob(Dispatchers.Default) {
totalPagesRead.value = repository.getTotalPagesRead(manga.id)
}
}
}

View File

@@ -25,9 +25,12 @@ import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.parsers.util.replaceWith
import org.koitharu.kotatsu.parsers.util.toIntUp
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.roundToInt
import kotlin.math.sqrt
import kotlin.random.Random
import com.google.android.material.R as materialR
class BarChartView @JvmOverloads constructor(
@@ -35,10 +38,12 @@ class BarChartView @JvmOverloads constructor(
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val rawData = ArrayList<Bar>()
private val bars = ArrayList<Bar>()
private var maxValue: Int = 0
private var spacing = context.resources.resolveDp(2f)
private val minBarSpacing = context.resources.resolveDp(12f)
private val minSpace = context.resources.resolveDp(20f)
private val barWidth = context.resources.resolveDp(12f)
private val outlineColor = context.getThemeColor(materialR.attr.colorOutline)
private val dottedEffect = DashPathEffect(
floatArrayOf(
@@ -50,7 +55,7 @@ class BarChartView @JvmOverloads constructor(
private val chartBounds = RectF()
@ColorInt
var barColor: Int = Color.MAGENTA
var barColor: Int = context.getThemeColor(materialR.attr.colorAccent)
set(value) {
field = value
invalidate()
@@ -58,6 +63,16 @@ class BarChartView @JvmOverloads constructor(
init {
paint.strokeWidth = context.resources.resolveDp(1f)
if (isInEditMode) {
setData(
List(Random.nextInt(20, 60)) {
Bar(
value = Random.nextInt(-20, 400).coerceAtLeast(0),
label = it.toString(),
)
},
)
}
}
override fun onDraw(canvas: Canvas) {
@@ -65,7 +80,7 @@ class BarChartView @JvmOverloads constructor(
if (bars.isEmpty() || chartBounds.isEmpty) {
return
}
val barWidth = ((chartBounds.width() + spacing) / bars.size.toFloat() - spacing)
val spacing = (chartBounds.width() - (barWidth * bars.size.toFloat())) / (bars.size + 1).toFloat()
// dashed horizontal lines
paint.color = outlineColor
paint.style = Paint.Style.STROKE
@@ -75,19 +90,23 @@ class BarChartView @JvmOverloads constructor(
val y = chartBounds.top + (chartBounds.height() * i / maxValue.toFloat())
canvas.drawLine(paddingLeft.toFloat(), y, (width - paddingLeft - paddingRight).toFloat(), y, paint)
}
// bars
paint.style = Paint.Style.FILL
paint.color = barColor
paint.pathEffect = null
for ((i, bar) in bars.withIndex()) {
val h = chartBounds.height() * bar.value / maxValue.toFloat()
val x = i * (barWidth + spacing) + paddingLeft
canvas.drawRect(x, chartBounds.bottom - h, x + barWidth, chartBounds.bottom, paint)
}
// bottom line
paint.color = outlineColor
paint.style = Paint.Style.STROKE
canvas.drawLine(chartBounds.left, chartBounds.bottom, chartBounds.right, chartBounds.bottom, paint)
// bars
paint.style = Paint.Style.FILL
paint.color = barColor
paint.pathEffect = null
val corner = barWidth / 2f
for ((i, bar) in bars.withIndex()) {
if (bar.value == 0) {
continue
}
val h = (chartBounds.height() * bar.value / maxValue.toFloat()).coerceAtLeast(barWidth)
val x = spacing + i * (barWidth + spacing) + paddingLeft
canvas.drawRoundRect(x, chartBounds.bottom - h, x + barWidth, chartBounds.bottom, corner, corner, paint)
}
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
@@ -96,11 +115,25 @@ class BarChartView @JvmOverloads constructor(
}
fun setData(value: List<Bar>) {
bars.replaceWith(value)
maxValue = if (value.isEmpty()) 0 else value.maxOf { it.value }
rawData.replaceWith(value)
compressBars()
invalidate()
}
private fun compressBars() {
if (rawData.isEmpty() || width <= 0) {
maxValue = 0
bars.clear()
return
}
var fullWidth = rawData.size * (barWidth + minBarSpacing) + minBarSpacing
val windowSize = (fullWidth / width.toFloat()).toIntUp()
bars.replaceWith(
rawData.chunked(windowSize) { it.average() },
)
maxValue = bars.maxOf { it.value }
}
private fun computeValueStep(): Int {
val h = chartBounds.height()
var step = 1
@@ -118,6 +151,18 @@ class BarChartView @JvmOverloads constructor(
(width - paddingLeft - paddingRight).toFloat() - inset,
(height - paddingTop - paddingBottom).toFloat() - inset,
)
compressBars()
}
private fun Collection<Bar>.average(): Bar {
return when (size) {
0 -> Bar(0, "")
1 -> first()
else -> Bar(
value = (sumOf { it.value } / size.toFloat()).roundToInt(),
label = "%s - %s".format(first().label, last().label),
)
}
}
class Bar(

View File

@@ -161,6 +161,7 @@ class PieChartView @JvmOverloads constructor(
val label: String,
val percent: Float,
val color: Int,
val tag: Any?,
)
interface OnSegmentClickListener {

View File

@@ -14,28 +14,69 @@
android:layout_height="wrap_content"
app:title="@string/reading_stats" />
<TextView
android:id="@+id/textView_title"
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/screen_padding"
android:textAppearance="?textAppearanceTitleMedium"
tools:text="@tools:sample/lorem[4]" />
android:scrollIndicators="top">
<org.koitharu.kotatsu.stats.ui.views.BarChartView
android:id="@+id/chartView"
android:layout_width="match_parent"
android:layout_height="240dp"
android:layout_marginTop="12dp"
android:paddingHorizontal="@dimen/screen_padding" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/textView_start"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:paddingHorizontal="@dimen/screen_padding"
android:textAppearance="?textAppearanceLabelSmall"
tools:text="Week ago" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/screen_padding">
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?textAppearanceTitleMedium"
tools:text="@tools:sample/lorem[4]" />
<ImageButton
android:id="@+id/button_open"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:minWidth="?minTouchTargetSize"
android:minHeight="?minTouchTargetSize"
app:srcCompat="@drawable/ic_open_external" />
</LinearLayout>
<org.koitharu.kotatsu.stats.ui.views.BarChartView
android:id="@+id/chartView"
android:layout_width="match_parent"
android:layout_height="240dp"
android:layout_marginTop="12dp"
android:paddingHorizontal="@dimen/screen_padding" />
<TextView
android:id="@+id/textView_start"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:paddingHorizontal="@dimen/screen_padding"
android:textAppearance="?textAppearanceLabelSmall"
tools:text="Week ago" />
<TextView
android:id="@+id/textView_pages"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:paddingHorizontal="@dimen/screen_padding"
android:textAppearance="?textAppearanceBodyMedium"
tools:text="Total pages read: 250" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -37,6 +37,12 @@
android:title="@string/tracking"
app:showAsAction="never" />
<item
android:id="@+id/action_stats"
android:orderInCategory="50"
android:title="@string/statistics"
app:showAsAction="never" />
<item
android:id="@+id/action_related"
android:orderInCategory="50"

View File

@@ -617,4 +617,5 @@
<string name="day">Day</string>
<string name="three_months">Three months</string>
<string name="empty_stats_text">There are no statistics for the selected period</string>
<string name="pages_read_s">Pages read: %s</string>
</resources>