Option to group history by date

pull/26/head
Koitharu 5 years ago
parent b1be45af8b
commit 9c20559962

@ -71,6 +71,7 @@ dependencies {
implementation 'androidx.activity:activity-ktx:1.2.0-beta01' implementation 'androidx.activity:activity-ktx:1.2.0-beta01'
implementation 'androidx.fragment:fragment-ktx:1.3.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-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.lifecycle:lifecycle-livedata-ktx:2.3.0-beta01'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'

@ -72,6 +72,8 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
val isPreferRtlReader by BoolPreferenceDelegate(KEY_READER_PREFER_RTL, false) val isPreferRtlReader by BoolPreferenceDelegate(KEY_READER_PREFER_RTL, false)
var historyGrouping by BoolPreferenceDelegate(KEY_HISTORY_GROUPING, true)
val zoomMode by EnumPreferenceDelegate( val zoomMode by EnumPreferenceDelegate(
ZoomMode::class.java, ZoomMode::class.java,
KEY_ZOOM_MODE, 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_ZOOM_MODE = "zoom_mode"
const val KEY_BACKUP = "backup" const val KEY_BACKUP = "backup"
const val KEY_RESTORE = "restore" 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?> { fun observeOne(id: Long): Flow<MangaHistory?> {
return db.historyDao.observe(id).map { return db.historyDao.observe(id).map {
it?.toMangaHistory() 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel.onItemRemoved.observe(viewLifecycleOwner, ::onItemRemoved) viewModel.onItemRemoved.observe(viewLifecycleOwner, ::onItemRemoved)
viewModel.isGroupingEnabled.observe(viewLifecycleOwner) {
activity?.invalidateOptionsMenu()
}
} }
override fun onScrolledToEnd() = Unit override fun onScrolledToEnd() = Unit
@ -31,6 +34,11 @@ class HistoryListFragment : MangaListFragment() {
super.onCreateOptionsMenu(menu, inflater) 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.action_clear_history -> { R.id.action_clear_history -> {
@ -43,12 +51,16 @@ class HistoryListFragment : MangaListFragment() {
}.show() }.show()
true true
} }
R.id.action_history_grouping -> {
viewModel.setGrouping(!item.isChecked)
true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
override fun getTitle(): CharSequence? { override fun getTitle(): CharSequence? {
return getString(R.string.history) return context?.getString(R.string.history)
} }
override fun setUpEmptyListHolder() { override fun setUpEmptyListHolder() {
@ -71,7 +83,7 @@ class HistoryListFragment : MangaListFragment() {
} }
} }
fun onItemRemoved(item: Manga) { private fun onItemRemoved(item: Manga) {
Snackbar.make( Snackbar.make(
recyclerView, getString( recyclerView, getString(
R.string._s_removed_from_history, R.string._s_removed_from_history,

@ -2,16 +2,17 @@ package org.koitharu.kotatsu.history.ui
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode 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.HistoryRepository
import org.koitharu.kotatsu.history.domain.MangaWithHistory
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.toGridModel import org.koitharu.kotatsu.list.ui.model.toGridModel
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
@ -22,22 +23,26 @@ import org.koitharu.kotatsu.utils.ext.onFirst
class HistoryListViewModel( class HistoryListViewModel(
private val repository: HistoryRepository, private val repository: HistoryRepository,
private val context: Context //todo create ShortcutRepository private val context: Context, //todo create ShortcutRepository
, settings: AppSettings private val settings: AppSettings
) : MangaListViewModel(settings) { ) : MangaListViewModel(settings) {
val onItemRemoved = SingleLiveEvent<Manga>() 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( override val content = combine(
repository.observeAll(), repository.observeAllWithHistory(),
createListModeFlow() historyGrouping,
) { list, mode -> createListModeFlow(),
when (mode) { ::mapList
ListMode.LIST -> list.map { it.toListModel() } ).onEach {
ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
ListMode.GRID -> list.map { it.toGridModel() }
}
}.onEach {
isEmptyState.postValue(it.isEmpty()) isEmptyState.postValue(it.isEmpty())
}.onStart { }.onStart {
isLoading.postValue(true) 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 { override fun getSpanSize(position: Int): Int {
val total = (recyclerView.layoutManager as? GridLayoutManager)?.spanCount ?: return 1 val total = (recyclerView.layoutManager as? GridLayoutManager)?.spanCount ?: return 1
return when (adapter?.getItemViewType(position)) { return when (adapter?.getItemViewType(position)) {
MangaListAdapter.ITEM_TYPE_DATE,
MangaListAdapter.ITEM_TYPE_PROGRESS -> total MangaListAdapter.ITEM_TYPE_PROGRESS -> total
else -> 1 else -> 1
} }

@ -5,6 +5,7 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.Manga 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.IndeterminateProgress
import org.koitharu.kotatsu.list.ui.model.MangaGridModel import org.koitharu.kotatsu.list.ui.model.MangaGridModel
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel 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_MANGA_GRID, mangaGridItemAD(coil, clickListener))
.addDelegate(ITEM_TYPE_PROGRESS, indeterminateProgressAD()) .addDelegate(ITEM_TYPE_PROGRESS, indeterminateProgressAD())
.addDelegate(ITEM_TYPE_DATE, relatedDateItemAD())
} }
private class DiffCallback : DiffUtil.ItemCallback<Any>() { private class DiffCallback : DiffUtil.ItemCallback<Any>() {
@ -41,6 +43,9 @@ class MangaListAdapter(
oldItem == IndeterminateProgress && newItem == IndeterminateProgress -> { oldItem == IndeterminateProgress && newItem == IndeterminateProgress -> {
true true
} }
oldItem is DateTimeAgo && newItem is DateTimeAgo -> {
oldItem == newItem
}
else -> false else -> false
} }
@ -55,5 +60,6 @@ class MangaListAdapter(
const val ITEM_TYPE_MANGA_LIST_DETAILED = 1 const val ITEM_TYPE_MANGA_LIST_DETAILED = 1
const val ITEM_TYPE_MANGA_GRID = 2 const val ITEM_TYPE_MANGA_GRID = 2
const val ITEM_TYPE_PROGRESS = 3 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 android.text.format.DateUtils
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
@SuppressLint("SimpleDateFormat") @SuppressLint("SimpleDateFormat")
fun Date.format(pattern: String): String = SimpleDateFormat(pattern).format(this) fun Date.format(pattern: String): String = SimpleDateFormat(pattern).format(this)
@ -15,3 +16,9 @@ fun Date.calendar(): Calendar = Calendar.getInstance().also {
fun Date.formatRelative(minResolution: Long): CharSequence = DateUtils.getRelativeTimeSpanString( fun Date.formatRelative(minResolution: Long): CharSequence = DateUtils.getRelativeTimeSpanString(
time, System.currentTimeMillis(), minResolution 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()
}

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

@ -3,6 +3,14 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> 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 <item
android:id="@+id/action_clear_history" android:id="@+id/action_clear_history"
android:orderInCategory="50" android:orderInCategory="50"

@ -25,4 +25,20 @@
<item quantity="few">%1$d главы из %2$d</item> <item quantity="few">%1$d главы из %2$d</item>
<item quantity="many">%1$d глав из %2$d</item> <item quantity="many">%1$d глав из %2$d</item>
</plurals> </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> </resources>

@ -180,4 +180,8 @@
<string name="data_restored_success">Все данные успешно восстановлены</string> <string name="data_restored_success">Все данные успешно восстановлены</string>
<string name="data_restored_with_errors">Данные восстановлены, но возникли некоторые ошибки</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="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> </resources>

@ -5,6 +5,6 @@
<dimen name="manga_list_details_item_height">120dp</dimen> <dimen name="manga_list_details_item_height">120dp</dimen>
<dimen name="chapter_list_item_height">46dp</dimen> <dimen name="chapter_list_item_height">46dp</dimen>
<dimen name="preferred_grid_width">120dp</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> <dimen name="elevation_large">16dp</dimen>
</resources> </resources>

@ -20,4 +20,16 @@
<item quantity="one">%1$d chapter from %2$d</item> <item quantity="one">%1$d chapter from %2$d</item>
<item quantity="other">%1$d chapters from %2$d</item> <item quantity="other">%1$d chapters from %2$d</item>
</plurals> </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> </resources>

@ -182,4 +182,8 @@
<string name="data_restored_success">All data restored successfully</string> <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="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="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> </resources>
Loading…
Cancel
Save