Option to group history by date
This commit is contained in:
@@ -72,6 +72,8 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
|
||||
|
||||
val isPreferRtlReader by BoolPreferenceDelegate(KEY_READER_PREFER_RTL, false)
|
||||
|
||||
var historyGrouping by BoolPreferenceDelegate(KEY_HISTORY_GROUPING, true)
|
||||
|
||||
val zoomMode by EnumPreferenceDelegate(
|
||||
ZoomMode::class.java,
|
||||
KEY_ZOOM_MODE,
|
||||
@@ -169,5 +171,6 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
|
||||
const val KEY_ZOOM_MODE = "zoom_mode"
|
||||
const val KEY_BACKUP = "backup"
|
||||
const val KEY_RESTORE = "restore"
|
||||
const val KEY_HISTORY_GROUPING = "history_grouping"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.content.res.Resources
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.utils.ext.daysDiff
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
sealed class DateTimeAgo {
|
||||
|
||||
abstract fun format(resources: Resources): String
|
||||
|
||||
object JustNow : DateTimeAgo() {
|
||||
override fun format(resources: Resources): String {
|
||||
return resources.getString(R.string.just_now)
|
||||
}
|
||||
}
|
||||
|
||||
data class MinutesAgo(val minutes: Int) : DateTimeAgo() {
|
||||
override fun format(resources: Resources): String {
|
||||
return resources.getQuantityString(R.plurals.minutes_ago, minutes, minutes)
|
||||
}
|
||||
}
|
||||
|
||||
data class HoursAgo(val hours: Int) : DateTimeAgo() {
|
||||
override fun format(resources: Resources): String {
|
||||
return resources.getQuantityString(R.plurals.hours_ago, hours, hours)
|
||||
}
|
||||
}
|
||||
|
||||
object Yesterday : DateTimeAgo() {
|
||||
override fun format(resources: Resources): String {
|
||||
return resources.getString(R.string.yesterday)
|
||||
}
|
||||
}
|
||||
|
||||
data class DaysAgo(val days: Int) : DateTimeAgo() {
|
||||
override fun format(resources: Resources): String {
|
||||
return resources.getQuantityString(R.plurals.days_ago, days, days)
|
||||
}
|
||||
}
|
||||
|
||||
object LongAgo : DateTimeAgo() {
|
||||
override fun format(resources: Resources): String {
|
||||
return resources.getString(R.string.long_ago)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(date: Date): DateTimeAgo {
|
||||
val diff = (System.currentTimeMillis() - date.time).coerceAtLeast(0L)
|
||||
val diffMinutes = TimeUnit.MILLISECONDS.toMinutes(diff).toInt()
|
||||
val diffHours = TimeUnit.MILLISECONDS.toHours(diff).toInt()
|
||||
val diffDays = -date.daysDiff(System.currentTimeMillis())
|
||||
return when {
|
||||
diffMinutes < 1 -> JustNow
|
||||
diffMinutes < 60 -> MinutesAgo(diffMinutes)
|
||||
diffDays < 1 -> HoursAgo(diffHours)
|
||||
diffDays == 1 -> Yesterday
|
||||
diffDays < 16 -> DaysAgo(diffDays)
|
||||
else -> LongAgo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,15 @@ class HistoryRepository(private val db: MangaDatabase) : KoinComponent {
|
||||
}
|
||||
}
|
||||
|
||||
fun observeAllWithHistory(): Flow<List<MangaWithHistory>> {
|
||||
return db.historyDao.observeAll().mapItems {
|
||||
MangaWithHistory(
|
||||
it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)),
|
||||
it.history.toMangaHistory()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun observeOne(id: Long): Flow<MangaHistory?> {
|
||||
return db.historyDao.observe(id).map {
|
||||
it?.toMangaHistory()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.history.domain
|
||||
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
|
||||
data class MangaWithHistory(
|
||||
val manga: Manga,
|
||||
val history: MangaHistory
|
||||
)
|
||||
@@ -22,6 +22,9 @@ class HistoryListFragment : MangaListFragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.onItemRemoved.observe(viewLifecycleOwner, ::onItemRemoved)
|
||||
viewModel.isGroupingEnabled.observe(viewLifecycleOwner) {
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() = Unit
|
||||
@@ -31,6 +34,11 @@ class HistoryListFragment : MangaListFragment() {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.action_history_grouping)?.isChecked = viewModel.isGroupingEnabled.value == true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_clear_history -> {
|
||||
@@ -43,12 +51,16 @@ class HistoryListFragment : MangaListFragment() {
|
||||
}.show()
|
||||
true
|
||||
}
|
||||
R.id.action_history_grouping -> {
|
||||
viewModel.setGrouping(!item.isChecked)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTitle(): CharSequence? {
|
||||
return getString(R.string.history)
|
||||
return context?.getString(R.string.history)
|
||||
}
|
||||
|
||||
override fun setUpEmptyListHolder() {
|
||||
@@ -71,7 +83,7 @@ class HistoryListFragment : MangaListFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onItemRemoved(item: Manga) {
|
||||
private fun onItemRemoved(item: Manga) {
|
||||
Snackbar.make(
|
||||
recyclerView, getString(
|
||||
R.string._s_removed_from_history,
|
||||
|
||||
@@ -2,16 +2,17 @@ package org.koitharu.kotatsu.history.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.domain.MangaWithHistory
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toGridModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
|
||||
@@ -22,22 +23,26 @@ import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
|
||||
class HistoryListViewModel(
|
||||
private val repository: HistoryRepository,
|
||||
private val context: Context //todo create ShortcutRepository
|
||||
, settings: AppSettings
|
||||
private val context: Context, //todo create ShortcutRepository
|
||||
private val settings: AppSettings
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
val onItemRemoved = SingleLiveEvent<Manga>()
|
||||
val isGroupingEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
private val historyGrouping = settings.observe()
|
||||
.filter { it == AppSettings.KEY_HISTORY_GROUPING }
|
||||
.map { settings.historyGrouping }
|
||||
.onStart { emit(settings.historyGrouping) }
|
||||
.distinctUntilChanged()
|
||||
.onEach { isGroupingEnabled.postValue(it) }
|
||||
|
||||
override val content = combine(
|
||||
repository.observeAll(),
|
||||
createListModeFlow()
|
||||
) { list, mode ->
|
||||
when (mode) {
|
||||
ListMode.LIST -> list.map { it.toListModel() }
|
||||
ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
|
||||
ListMode.GRID -> list.map { it.toGridModel() }
|
||||
}
|
||||
}.onEach {
|
||||
repository.observeAllWithHistory(),
|
||||
historyGrouping,
|
||||
createListModeFlow(),
|
||||
::mapList
|
||||
).onEach {
|
||||
isEmptyState.postValue(it.isEmpty())
|
||||
}.onStart {
|
||||
isLoading.postValue(true)
|
||||
@@ -64,4 +69,27 @@ class HistoryListViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun setGrouping(isGroupingEnabled: Boolean) {
|
||||
settings.historyGrouping = isGroupingEnabled
|
||||
}
|
||||
|
||||
private fun mapList(list: List<MangaWithHistory>, grouped: Boolean, mode: ListMode): List<Any> {
|
||||
val result = ArrayList<Any>((list.size * 1.4).toInt())
|
||||
var prevDate: DateTimeAgo? = null
|
||||
for ((manga, history) in list) {
|
||||
if (grouped) {
|
||||
val date = DateTimeAgo.from(history.updatedAt)
|
||||
if (prevDate != date) {
|
||||
result += date
|
||||
}
|
||||
prevDate = date
|
||||
}
|
||||
result += when (mode) {
|
||||
ListMode.LIST -> manga.toListModel()
|
||||
ListMode.DETAILED_LIST -> manga.toListDetailedModel()
|
||||
ListMode.GRID -> manga.toGridModel()
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -273,6 +273,7 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
val total = (recyclerView.layoutManager as? GridLayoutManager)?.spanCount ?: return 1
|
||||
return when (adapter?.getItemViewType(position)) {
|
||||
MangaListAdapter.ITEM_TYPE_DATE,
|
||||
MangaListAdapter.ITEM_TYPE_PROGRESS -> total
|
||||
else -> 1
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
||||
import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
|
||||
@@ -24,6 +25,7 @@ class MangaListAdapter(
|
||||
)
|
||||
.addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, clickListener))
|
||||
.addDelegate(ITEM_TYPE_PROGRESS, indeterminateProgressAD())
|
||||
.addDelegate(ITEM_TYPE_DATE, relatedDateItemAD())
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<Any>() {
|
||||
@@ -41,6 +43,9 @@ class MangaListAdapter(
|
||||
oldItem == IndeterminateProgress && newItem == IndeterminateProgress -> {
|
||||
true
|
||||
}
|
||||
oldItem is DateTimeAgo && newItem is DateTimeAgo -> {
|
||||
oldItem == newItem
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
@@ -55,5 +60,6 @@ class MangaListAdapter(
|
||||
const val ITEM_TYPE_MANGA_LIST_DETAILED = 1
|
||||
const val ITEM_TYPE_MANGA_GRID = 2
|
||||
const val ITEM_TYPE_PROGRESS = 3
|
||||
const val ITEM_TYPE_DATE = 4
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import android.widget.TextView
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
||||
|
||||
fun relatedDateItemAD() = adapterDelegate<DateTimeAgo, Any>(R.layout.item_header) {
|
||||
|
||||
bind {
|
||||
(itemView as TextView).text = item.format(context.resources)
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.text.format.DateUtils
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
fun Date.format(pattern: String): String = SimpleDateFormat(pattern).format(this)
|
||||
@@ -14,4 +15,10 @@ fun Date.calendar(): Calendar = Calendar.getInstance().also {
|
||||
|
||||
fun Date.formatRelative(minResolution: Long): CharSequence = DateUtils.getRelativeTimeSpanString(
|
||||
time, System.currentTimeMillis(), minResolution
|
||||
)
|
||||
)
|
||||
|
||||
fun Date.daysDiff(other: Long): Int {
|
||||
val thisDay = time / TimeUnit.DAYS.toMillis(1L)
|
||||
val otherDay = other/ TimeUnit.DAYS.toMillis(1L)
|
||||
return (thisDay - otherDay).toInt()
|
||||
}
|
||||
Reference in New Issue
Block a user