Stats chart for single manga
This commit is contained in:
@@ -12,12 +12,10 @@ import android.graphics.RectF
|
|||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import androidx.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import androidx.core.content.withStyledAttributes
|
import androidx.core.content.withStyledAttributes
|
||||||
import androidx.core.graphics.ColorUtils
|
|
||||||
import androidx.core.graphics.withClip
|
import androidx.core.graphics.withClip
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.util.Colors
|
import org.koitharu.kotatsu.core.util.KotatsuColors
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
|
|
||||||
class FaviconDrawable(
|
class FaviconDrawable(
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -45,7 +43,7 @@ class FaviconDrawable(
|
|||||||
}
|
}
|
||||||
paint.textAlign = Paint.Align.CENTER
|
paint.textAlign = Paint.Align.CENTER
|
||||||
paint.isFakeBoldText = true
|
paint.isFakeBoldText = true
|
||||||
colorForeground = MaterialColors.harmonize(Colors.random(name), colorBackground)
|
colorForeground = MaterialColors.harmonize(KotatsuColors.random(name), colorBackground)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun draw(canvas: Canvas) {
|
override fun draw(canvas: Canvas) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
|||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
object Colors {
|
object KotatsuColors {
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
fun segmentColor(context: Context, @AttrRes resId: Int): Int {
|
fun segmentColor(context: Context, @AttrRes resId: Int): Int {
|
||||||
@@ -28,7 +28,7 @@ object Colors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
fun of(context: Context, manga: Manga?): Int {
|
fun ofManga(context: Context, manga: Manga?): Int {
|
||||||
val color = if (manga != null) {
|
val color = if (manga != null) {
|
||||||
val hue = (manga.id.absoluteValue % 360).toFloat()
|
val hue = (manga.id.absoluteValue % 360).toFloat()
|
||||||
ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))
|
ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))
|
||||||
@@ -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.onShowTip.observeEvent(this) { showTip() }
|
||||||
viewModel.historyInfo.observe(this, ::onHistoryChanged)
|
viewModel.historyInfo.observe(this, ::onHistoryChanged)
|
||||||
viewModel.selectedBranch.observe(this) {
|
viewModel.selectedBranch.observe(this) {
|
||||||
@@ -150,6 +153,7 @@ class DetailsActivity :
|
|||||||
viewModel.isChaptersEmpty.observe(this, chaptersMenuInvalidator)
|
viewModel.isChaptersEmpty.observe(this, chaptersMenuInvalidator)
|
||||||
val menuInvalidator = MenuInvalidator(this)
|
val menuInvalidator = MenuInvalidator(this)
|
||||||
viewModel.favouriteCategories.observe(this, menuInvalidator)
|
viewModel.favouriteCategories.observe(this, menuInvalidator)
|
||||||
|
viewModel.isStatsEnabled.observe(this, menuInvalidator)
|
||||||
viewModel.remoteManga.observe(this, menuInvalidator)
|
viewModel.remoteManga.observe(this, menuInvalidator)
|
||||||
viewModel.branches.observe(this) {
|
viewModel.branches.observe(this) {
|
||||||
viewBinding.buttonDropdown.isVisible = it.size > 1
|
viewBinding.buttonDropdown.isVisible = it.size > 1
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
||||||
|
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
|
||||||
|
|
||||||
class DetailsMenuProvider(
|
class DetailsMenuProvider(
|
||||||
private val activity: FragmentActivity,
|
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_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity)
|
||||||
menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable
|
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_online).isVisible = viewModel.remoteManga.value != null
|
||||||
|
menu.findItem(R.id.action_stats).isVisible = viewModel.isStatsEnabled.value
|
||||||
menu.findItem(R.id.action_favourite).setIcon(
|
menu.findItem(R.id.action_favourite).setIcon(
|
||||||
if (viewModel.favouriteCategories.value) R.drawable.ic_heart else R.drawable.ic_heart_outline,
|
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 -> {
|
R.id.action_scrobbling -> {
|
||||||
viewModel.manga.value?.let {
|
viewModel.manga.value?.let {
|
||||||
ScrobblingSelectorSheet.show(activity.supportFragmentManager, it, null)
|
ScrobblingSelectorSheet.show(activity.supportFragmentManager, it, null)
|
||||||
|
|||||||
@@ -100,6 +100,10 @@ class DetailsViewModel @Inject constructor(
|
|||||||
val favouriteCategories = interactor.observeIsFavourite(mangaId)
|
val favouriteCategories = interactor.observeIsFavourite(mangaId)
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
.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 remoteManga = MutableStateFlow<Manga?>(null)
|
||||||
|
|
||||||
val newChaptersCount = details.flatMapLatest { d ->
|
val newChaptersCount = details.flatMapLatest { d ->
|
||||||
|
|||||||
@@ -2,18 +2,11 @@ package org.koitharu.kotatsu.local.ui.info
|
|||||||
|
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
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.core.widget.TextViewCompat
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import com.google.android.material.color.MaterialColors
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.combine
|
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.model.parcelable.ParcelableManga
|
||||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||||
import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView
|
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.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.observe
|
||||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||||
import org.koitharu.kotatsu.databinding.DialogLocalInfoBinding
|
import org.koitharu.kotatsu.databinding.DialogLocalInfoBinding
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.settings.userdata.StorageUsage
|
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -67,7 +57,7 @@ class LocalInfoDialog : AlertDialogFragment<DialogLocalInfoBinding>() {
|
|||||||
val total = size + available
|
val total = size + available
|
||||||
val segment = SegmentedBarView.Segment(
|
val segment = SegmentedBarView.Segment(
|
||||||
percent = (size.toDouble() / total.toDouble()).toFloat(),
|
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(
|
requireViewBinding().labelUsed.text = view.context.getString(
|
||||||
R.string.memory_usage_pattern,
|
R.string.memory_usage_pattern,
|
||||||
|
|||||||
@@ -3,20 +3,15 @@ package org.koitharu.kotatsu.settings.userdata
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.annotation.AttrRes
|
|
||||||
import androidx.annotation.ColorInt
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.graphics.ColorUtils
|
|
||||||
import androidx.core.widget.TextViewCompat
|
import androidx.core.widget.TextViewCompat
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceViewHolder
|
import androidx.preference.PreferenceViewHolder
|
||||||
import com.google.android.material.color.MaterialColors
|
|
||||||
import kotlinx.coroutines.flow.FlowCollector
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView
|
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.FileSize
|
||||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
|
||||||
import org.koitharu.kotatsu.databinding.PreferenceMemoryUsageBinding
|
import org.koitharu.kotatsu.databinding.PreferenceMemoryUsageBinding
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
@@ -39,15 +34,15 @@ class StorageUsagePreference @JvmOverloads constructor(
|
|||||||
val binding = PreferenceMemoryUsageBinding.bind(holder.itemView)
|
val binding = PreferenceMemoryUsageBinding.bind(holder.itemView)
|
||||||
val storageSegment = SegmentedBarView.Segment(
|
val storageSegment = SegmentedBarView.Segment(
|
||||||
usage?.savedManga?.percent ?: 0f,
|
usage?.savedManga?.percent ?: 0f,
|
||||||
Colors.segmentColor(context, materialR.attr.colorPrimary),
|
KotatsuColors.segmentColor(context, materialR.attr.colorPrimary),
|
||||||
)
|
)
|
||||||
val pagesSegment = SegmentedBarView.Segment(
|
val pagesSegment = SegmentedBarView.Segment(
|
||||||
usage?.pagesCache?.percent ?: 0f,
|
usage?.pagesCache?.percent ?: 0f,
|
||||||
Colors.segmentColor(context, materialR.attr.colorSecondary),
|
KotatsuColors.segmentColor(context, materialR.attr.colorSecondary),
|
||||||
)
|
)
|
||||||
val otherSegment = SegmentedBarView.Segment(
|
val otherSegment = SegmentedBarView.Segment(
|
||||||
usage?.otherCache?.percent ?: 0f,
|
usage?.otherCache?.percent ?: 0f,
|
||||||
Colors.segmentColor(context, materialR.attr.colorTertiary),
|
KotatsuColors.segmentColor(context, materialR.attr.colorTertiary),
|
||||||
)
|
)
|
||||||
|
|
||||||
with(binding) {
|
with(binding) {
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ class StatsRepository @Inject constructor(
|
|||||||
time
|
time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getTotalPagesRead(mangaId: Long): Int {
|
||||||
|
return db.getStatsDao().getReadPagesCount(mangaId)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getMangaTimeline(mangaId: Long): NavigableMap<Long, Int> {
|
suspend fun getMangaTimeline(mangaId: Long): NavigableMap<Long, Int> {
|
||||||
val entities = db.getStatsDao().findAll(mangaId)
|
val entities = db.getStatsDao().findAll(mangaId)
|
||||||
val map = TreeMap<Long, Int>()
|
val map = TreeMap<Long, Int>()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.content.res.ColorStateList
|
|||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
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.databinding.ItemStatsBinding
|
||||||
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
|
||||||
@@ -22,7 +22,7 @@ fun statsAD(
|
|||||||
bind {
|
bind {
|
||||||
binding.textViewTitle.text = item.manga?.title ?: getString(R.string.other_manga)
|
binding.textViewTitle.text = item.manga?.title ?: getString(R.string.other_manga)
|
||||||
binding.textViewSummary.text = item.time.format(context.resources)
|
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
|
binding.root.isClickable = item.manga != null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,29 @@
|
|||||||
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.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewStub
|
import android.view.ViewStub
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.PopupMenu
|
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.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.recyclerview.widget.AsyncListDiffer
|
import androidx.recyclerview.widget.AsyncListDiffer
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
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.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.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.DIALOG_THEME_CENTERED
|
||||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
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.newImageRequest
|
||||||
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
|
||||||
@@ -43,7 +31,6 @@ import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
|||||||
import org.koitharu.kotatsu.core.util.ext.showOrHide
|
import org.koitharu.kotatsu.core.util.ext.showOrHide
|
||||||
import org.koitharu.kotatsu.databinding.ActivityStatsBinding
|
import org.koitharu.kotatsu.databinding.ActivityStatsBinding
|
||||||
import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
|
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.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.StatsPeriod
|
||||||
@@ -90,7 +77,8 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(),
|
|||||||
value = (v.duration / 1000).toInt(),
|
value = (v.duration / 1000).toInt(),
|
||||||
label = v.manga?.title ?: getString(R.string.other_manga),
|
label = v.manga?.title ?: getString(R.string.other_manga),
|
||||||
percent = (v.duration.toDouble() / sum).toFloat(),
|
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) {
|
override fun onSegmentClick(view: PieChartView, segment: PieChartView.Segment) {
|
||||||
Toast.makeText(this, segment.label, Toast.LENGTH_SHORT).apply {
|
val manga = segment.tag as? Manga ?: return
|
||||||
setGravity(Gravity.TOP, 0, view.top + view.height / 2)
|
onItemClick(manga, view)
|
||||||
}.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
|||||||
@@ -2,27 +2,28 @@ package org.koitharu.kotatsu.stats.ui.sheet
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.collection.IntList
|
import androidx.collection.IntList
|
||||||
import androidx.collection.LongIntMap
|
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.model.parcelable.ParcelableManga
|
||||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
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.observe
|
||||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||||
import org.koitharu.kotatsu.databinding.SheetStatsMangaBinding
|
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.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.util.format
|
||||||
import org.koitharu.kotatsu.stats.ui.views.BarChartView
|
import org.koitharu.kotatsu.stats.ui.views.BarChartView
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MangaStatsSheet : BaseAdaptiveSheet<SheetStatsMangaBinding>() {
|
class MangaStatsSheet : BaseAdaptiveSheet<SheetStatsMangaBinding>(), View.OnClickListener {
|
||||||
|
|
||||||
private val viewModel: MangaStatsViewModel by viewModels()
|
private val viewModel: MangaStatsViewModel by viewModels()
|
||||||
|
|
||||||
@@ -33,11 +34,19 @@ class MangaStatsSheet : BaseAdaptiveSheet<SheetStatsMangaBinding>() {
|
|||||||
override fun onViewBindingCreated(binding: SheetStatsMangaBinding, savedInstanceState: Bundle?) {
|
override fun onViewBindingCreated(binding: SheetStatsMangaBinding, savedInstanceState: Bundle?) {
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
binding.textViewTitle.text = viewModel.manga.title
|
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.stats.observe(viewLifecycleOwner, ::onStatsChanged)
|
||||||
viewModel.startDate.observe(viewLifecycleOwner) {
|
viewModel.startDate.observe(viewLifecycleOwner) {
|
||||||
binding.textViewStart.textAndVisible = it?.format(resources)
|
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) {
|
private fun onStatsChanged(stats: IntList) {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class MangaStatsViewModel @Inject constructor(
|
|||||||
|
|
||||||
val stats = MutableStateFlow<IntList>(emptyIntList())
|
val stats = MutableStateFlow<IntList>(emptyIntList())
|
||||||
val startDate = MutableStateFlow<DateTimeAgo?>(null)
|
val startDate = MutableStateFlow<DateTimeAgo?>(null)
|
||||||
|
val totalPagesRead = MutableStateFlow(0)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
@@ -39,7 +40,7 @@ class MangaStatsViewModel @Inject constructor(
|
|||||||
stats.value = emptyIntList()
|
stats.value = emptyIntList()
|
||||||
} else {
|
} else {
|
||||||
val startDay = TimeUnit.MILLISECONDS.toDays(timeline.firstKey())
|
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)
|
val res = MutableIntList((endDay - startDay).toInt() + 1)
|
||||||
for (day in startDay..endDay) {
|
for (day in startDay..endDay) {
|
||||||
val from = TimeUnit.DAYS.toMillis(day)
|
val from = TimeUnit.DAYS.toMillis(day)
|
||||||
@@ -50,5 +51,8 @@ class MangaStatsViewModel @Inject constructor(
|
|||||||
startDate.value = calculateTimeAgo(Instant.ofEpochMilli(timeline.firstKey()))
|
startDate.value = calculateTimeAgo(Instant.ofEpochMilli(timeline.firstKey()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
|
totalPagesRead.value = repository.getTotalPagesRead(manga.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.getThemeColor
|
||||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||||
import org.koitharu.kotatsu.parsers.util.replaceWith
|
import org.koitharu.kotatsu.parsers.util.replaceWith
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
import kotlin.random.Random
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
class BarChartView @JvmOverloads constructor(
|
class BarChartView @JvmOverloads constructor(
|
||||||
@@ -35,10 +38,12 @@ class BarChartView @JvmOverloads constructor(
|
|||||||
) : View(context, attrs, defStyleAttr) {
|
) : View(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
private val rawData = ArrayList<Bar>()
|
||||||
private val bars = ArrayList<Bar>()
|
private val bars = ArrayList<Bar>()
|
||||||
private var maxValue: Int = 0
|
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 minSpace = context.resources.resolveDp(20f)
|
||||||
|
private val barWidth = context.resources.resolveDp(12f)
|
||||||
private val outlineColor = context.getThemeColor(materialR.attr.colorOutline)
|
private val outlineColor = context.getThemeColor(materialR.attr.colorOutline)
|
||||||
private val dottedEffect = DashPathEffect(
|
private val dottedEffect = DashPathEffect(
|
||||||
floatArrayOf(
|
floatArrayOf(
|
||||||
@@ -50,7 +55,7 @@ class BarChartView @JvmOverloads constructor(
|
|||||||
private val chartBounds = RectF()
|
private val chartBounds = RectF()
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
var barColor: Int = Color.MAGENTA
|
var barColor: Int = context.getThemeColor(materialR.attr.colorAccent)
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
invalidate()
|
invalidate()
|
||||||
@@ -58,6 +63,16 @@ class BarChartView @JvmOverloads constructor(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
paint.strokeWidth = context.resources.resolveDp(1f)
|
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) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
@@ -65,7 +80,7 @@ class BarChartView @JvmOverloads constructor(
|
|||||||
if (bars.isEmpty() || chartBounds.isEmpty) {
|
if (bars.isEmpty() || chartBounds.isEmpty) {
|
||||||
return
|
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
|
// dashed horizontal lines
|
||||||
paint.color = outlineColor
|
paint.color = outlineColor
|
||||||
paint.style = Paint.Style.STROKE
|
paint.style = Paint.Style.STROKE
|
||||||
@@ -75,19 +90,23 @@ class BarChartView @JvmOverloads constructor(
|
|||||||
val y = chartBounds.top + (chartBounds.height() * i / maxValue.toFloat())
|
val y = chartBounds.top + (chartBounds.height() * i / maxValue.toFloat())
|
||||||
canvas.drawLine(paddingLeft.toFloat(), y, (width - paddingLeft - paddingRight).toFloat(), y, paint)
|
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
|
// bottom line
|
||||||
paint.color = outlineColor
|
paint.color = outlineColor
|
||||||
paint.style = Paint.Style.STROKE
|
paint.style = Paint.Style.STROKE
|
||||||
canvas.drawLine(chartBounds.left, chartBounds.bottom, chartBounds.right, chartBounds.bottom, paint)
|
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) {
|
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>) {
|
fun setData(value: List<Bar>) {
|
||||||
bars.replaceWith(value)
|
rawData.replaceWith(value)
|
||||||
maxValue = if (value.isEmpty()) 0 else value.maxOf { it.value }
|
compressBars()
|
||||||
invalidate()
|
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 {
|
private fun computeValueStep(): Int {
|
||||||
val h = chartBounds.height()
|
val h = chartBounds.height()
|
||||||
var step = 1
|
var step = 1
|
||||||
@@ -118,6 +151,18 @@ class BarChartView @JvmOverloads constructor(
|
|||||||
(width - paddingLeft - paddingRight).toFloat() - inset,
|
(width - paddingLeft - paddingRight).toFloat() - inset,
|
||||||
(height - paddingTop - paddingBottom).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(
|
class Bar(
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ class PieChartView @JvmOverloads constructor(
|
|||||||
val label: String,
|
val label: String,
|
||||||
val percent: Float,
|
val percent: Float,
|
||||||
val color: Int,
|
val color: Int,
|
||||||
|
val tag: Any?,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface OnSegmentClickListener {
|
interface OnSegmentClickListener {
|
||||||
|
|||||||
@@ -14,28 +14,69 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:title="@string/reading_stats" />
|
app:title="@string/reading_stats" />
|
||||||
|
|
||||||
<TextView
|
<ScrollView
|
||||||
android:id="@+id/textView_title"
|
android:id="@+id/scrollView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingHorizontal="@dimen/screen_padding"
|
android:scrollIndicators="top">
|
||||||
android:textAppearance="?textAppearanceTitleMedium"
|
|
||||||
tools:text="@tools:sample/lorem[4]" />
|
|
||||||
|
|
||||||
<org.koitharu.kotatsu.stats.ui.views.BarChartView
|
<LinearLayout
|
||||||
android:id="@+id/chartView"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="240dp"
|
android:orientation="vertical">
|
||||||
android:layout_marginTop="12dp"
|
|
||||||
android:paddingHorizontal="@dimen/screen_padding" />
|
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/textView_start"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:gravity="center_vertical"
|
||||||
android:layout_marginTop="2dp"
|
android:orientation="horizontal"
|
||||||
android:paddingHorizontal="@dimen/screen_padding"
|
android:paddingHorizontal="@dimen/screen_padding">
|
||||||
android:textAppearance="?textAppearanceLabelSmall"
|
|
||||||
tools:text="Week ago" />
|
|
||||||
|
|
||||||
|
<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>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -37,6 +37,12 @@
|
|||||||
android:title="@string/tracking"
|
android:title="@string/tracking"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_stats"
|
||||||
|
android:orderInCategory="50"
|
||||||
|
android:title="@string/statistics"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_related"
|
android:id="@+id/action_related"
|
||||||
android:orderInCategory="50"
|
android:orderInCategory="50"
|
||||||
|
|||||||
@@ -617,4 +617,5 @@
|
|||||||
<string name="day">Day</string>
|
<string name="day">Day</string>
|
||||||
<string name="three_months">Three months</string>
|
<string name="three_months">Three months</string>
|
||||||
<string name="empty_stats_text">There are no statistics for the selected period</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>
|
</resources>
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ class JsonSerializerTest {
|
|||||||
scroll = 24.0f,
|
scroll = 24.0f,
|
||||||
percent = 0.6f,
|
percent = 0.6f,
|
||||||
deletedAt = 0L,
|
deletedAt = 0L,
|
||||||
|
chaptersCount = 12,
|
||||||
)
|
)
|
||||||
val json = JsonSerializer(entity).toJson()
|
val json = JsonSerializer(entity).toJson()
|
||||||
val result = JsonDeserializer(json).toHistoryEntity()
|
val result = JsonDeserializer(json).toHistoryEntity()
|
||||||
|
|||||||
Reference in New Issue
Block a user