Option to group history by date

This commit is contained in:
Koitharu
2020-11-28 14:02:03 +02:00
parent b1be45af8b
commit 9c20559962
18 changed files with 219 additions and 20 deletions

View File

@@ -71,6 +71,7 @@ dependencies {
implementation 'androidx.activity:activity-ktx:1.2.0-beta01'
implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-beta01'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-beta01'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-beta01'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'

View File

@@ -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"
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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()

View File

@@ -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
)

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}

View File

@@ -3,11 +3,11 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/header_height"
android:background="?android:windowBackground"
android:layout_height="wrap_content"
android:gravity="center_vertical|start"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:minHeight="@dimen/header_height"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
android:textColor="?android:textColorSecondary"

View File

@@ -3,6 +3,14 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_history_grouping"
android:checkable="true"
android:checked="true"
android:orderInCategory="15"
android:title="@string/group"
app:showAsAction="never" />
<item
android:id="@+id/action_clear_history"
android:orderInCategory="50"

View File

@@ -25,4 +25,20 @@
<item quantity="few">%1$d главы из %2$d</item>
<item quantity="many">%1$d глав из %2$d</item>
</plurals>
<plurals name="minutes_ago">
<item quantity="one">%1$d минуту назад</item>
<item quantity="few">%1$d минуты назад</item>
<item quantity="many">%1$d минут назад</item>
</plurals>
<plurals name="hours_ago">
<item quantity="one">%1$d час назад</item>
<item quantity="few">%1$d часа назад</item>
<item quantity="many">%1$d часов назад</item>
</plurals>
<plurals name="days_ago">
<item quantity="one">%1$d день назад</item>
<item quantity="few">%1$d дня назад</item>
<item quantity="many">%1$d дней назад</item>
</plurals>
</resources>

View File

@@ -180,4 +180,8 @@
<string name="data_restored_success">Все данные успешно восстановлены</string>
<string name="data_restored_with_errors">Данные восстановлены, но возникли некоторые ошибки</string>
<string name="backup_information">You can create backup of your history and favourites and restore it</string>
<string name="just_now">Только что</string>
<string name="yesterday">Вчера</string>
<string name="long_ago">Давно</string>
<string name="group">Группировать</string>
</resources>

View File

@@ -5,6 +5,6 @@
<dimen name="manga_list_details_item_height">120dp</dimen>
<dimen name="chapter_list_item_height">46dp</dimen>
<dimen name="preferred_grid_width">120dp</dimen>
<dimen name="header_height">42dp</dimen>
<dimen name="header_height">34dp</dimen>
<dimen name="elevation_large">16dp</dimen>
</resources>

View File

@@ -20,4 +20,16 @@
<item quantity="one">%1$d chapter from %2$d</item>
<item quantity="other">%1$d chapters from %2$d</item>
</plurals>
<plurals name="minutes_ago">
<item quantity="one">%1$d minute ago</item>
<item quantity="other">%1$d minutes ago</item>
</plurals>
<plurals name="hours_ago">
<item quantity="one">%1$d hour ago</item>
<item quantity="other">%1$d hours ago</item>
</plurals>
<plurals name="days_ago">
<item quantity="one">%1$d day ago</item>
<item quantity="other">%1$d days ago</item>
</plurals>
</resources>

View File

@@ -182,4 +182,8 @@
<string name="data_restored_success">All data restored successfully</string>
<string name="data_restored_with_errors">The data restored, but there are errors</string>
<string name="backup_information">You can create backup of your history and favourites and restore it</string>
<string name="just_now">Just now</string>
<string name="yesterday">Yesterday</string>
<string name="long_ago">Long ago</string>
<string name="group">Group</string>
</resources>