From 9c2055996216e029b0fc73037ef0fc44159ba279 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 28 Nov 2020 14:02:03 +0200 Subject: [PATCH] Option to group history by date --- app/build.gradle | 1 + .../kotatsu/core/prefs/AppSettings.kt | 3 + .../koitharu/kotatsu/core/ui/DateTimeAgo.kt | 66 +++++++++++++++++++ .../history/domain/HistoryRepository.kt | 9 +++ .../history/domain/MangaWithHistory.kt | 9 +++ .../kotatsu/history/ui/HistoryListFragment.kt | 16 ++++- .../history/ui/HistoryListViewModel.kt | 56 ++++++++++++---- .../kotatsu/list/ui/MangaListFragment.kt | 1 + .../list/ui/adapter/MangaListAdapter.kt | 6 ++ .../list/ui/adapter/RelatedDateItemAD.kt | 13 ++++ .../org/koitharu/kotatsu/utils/ext/DateExt.kt | 9 ++- app/src/main/res/layout/item_header.xml | 4 +- app/src/main/res/menu/opt_history.xml | 8 +++ app/src/main/res/values-ru/plurals.xml | 16 +++++ app/src/main/res/values-ru/strings.xml | 4 ++ app/src/main/res/values/dimens.xml | 2 +- app/src/main/res/values/plurals.xml | 12 ++++ app/src/main/res/values/strings.xml | 4 ++ 18 files changed, 219 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/ui/DateTimeAgo.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/history/domain/MangaWithHistory.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/RelatedDateItemAD.kt diff --git a/app/build.gradle b/app/build.gradle index 1daeefc60..5c8844638 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 9cc5460bb..6a2967f3e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -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" } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/DateTimeAgo.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/DateTimeAgo.kt new file mode 100644 index 000000000..1ed9772b0 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/DateTimeAgo.kt @@ -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 + } + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt index 76f98590a..f123765fe 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt @@ -33,6 +33,15 @@ class HistoryRepository(private val db: MangaDatabase) : KoinComponent { } } + fun observeAllWithHistory(): Flow> { + return db.historyDao.observeAll().mapItems { + MangaWithHistory( + it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)), + it.history.toMangaHistory() + ) + } + } + fun observeOne(id: Long): Flow { return db.historyDao.observe(id).map { it?.toMangaHistory() diff --git a/app/src/main/java/org/koitharu/kotatsu/history/domain/MangaWithHistory.kt b/app/src/main/java/org/koitharu/kotatsu/history/domain/MangaWithHistory.kt new file mode 100644 index 000000000..8b657599d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/history/domain/MangaWithHistory.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt index 2dcc7cefb..1aef5c78f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt @@ -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, diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index d31ccfa0b..8ab56809c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -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() + val isGroupingEnabled = MutableLiveData() + + 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, grouped: Boolean, mode: ListMode): List { + val result = ArrayList((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 + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 1ea0a3398..a40b68c1d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -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 } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt index 343e691ad..73210315f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt @@ -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() { @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/RelatedDateItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/RelatedDateItemAD.kt new file mode 100644 index 000000000..065d9efd6 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/RelatedDateItemAD.kt @@ -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(R.layout.item_header) { + + bind { + (itemView as TextView).text = item.format(context.resources) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/DateExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/DateExt.kt index d994c3790..c67bba6f8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/DateExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/DateExt.kt @@ -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 -) \ No newline at end of file +) + +fun Date.daysDiff(other: Long): Int { + val thisDay = time / TimeUnit.DAYS.toMillis(1L) + val otherDay = other/ TimeUnit.DAYS.toMillis(1L) + return (thisDay - otherDay).toInt() +} \ No newline at end of file diff --git a/app/src/main/res/layout/item_header.xml b/app/src/main/res/layout/item_header.xml index 3598ed294..7031727d6 100644 --- a/app/src/main/res/layout/item_header.xml +++ b/app/src/main/res/layout/item_header.xml @@ -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" diff --git a/app/src/main/res/menu/opt_history.xml b/app/src/main/res/menu/opt_history.xml index f1cd6968b..13dead55b 100644 --- a/app/src/main/res/menu/opt_history.xml +++ b/app/src/main/res/menu/opt_history.xml @@ -3,6 +3,14 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> + + %1$d главы из %2$d %1$d глав из %2$d + + + %1$d минуту назад + %1$d минуты назад + %1$d минут назад + + + %1$d час назад + %1$d часа назад + %1$d часов назад + + + %1$d день назад + %1$d дня назад + %1$d дней назад + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 62b4495e8..65f946c7c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -180,4 +180,8 @@ Все данные успешно восстановлены Данные восстановлены, но возникли некоторые ошибки You can create backup of your history and favourites and restore it + Только что + Вчера + Давно + Группировать \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 1b8c917a4..558c95b14 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -5,6 +5,6 @@ 120dp 46dp 120dp - 42dp + 34dp 16dp \ No newline at end of file diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml index 713132a02..88d65c476 100644 --- a/app/src/main/res/values/plurals.xml +++ b/app/src/main/res/values/plurals.xml @@ -20,4 +20,16 @@ %1$d chapter from %2$d %1$d chapters from %2$d + + %1$d minute ago + %1$d minutes ago + + + %1$d hour ago + %1$d hours ago + + + %1$d day ago + %1$d days ago + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f4b5c380b..3e9dd2e88 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -182,4 +182,8 @@ All data restored successfully The data restored, but there are errors You can create backup of your history and favourites and restore it + Just now + Yesterday + Long ago + Group \ No newline at end of file