From 7c7106a63c60232dc8e23e477376ba57ece3578d Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 25 Jul 2023 15:36:50 +0300 Subject: [PATCH] History sorting #428 --- .../kotatsu/core/prefs/AppSettings.kt | 6 +++ .../kotatsu/core/ui/util/MenuInvalidator.kt | 13 ++++++ .../kotatsu/history/data/HistoryDao.kt | 25 +++++++++++ .../kotatsu/history/data/HistoryRepository.kt | 5 ++- .../history/domain/model/HistoryOrder.kt | 16 +++++++ .../kotatsu/history/ui/HistoryListFragment.kt | 7 +-- .../history/ui/HistoryListMenuProvider.kt | 43 ++++++++++++++----- .../history/ui/HistoryListViewModel.kt | 25 +++++++++-- app/src/main/res/menu/opt_history.xml | 13 ++++++ app/src/main/res/values/strings.xml | 2 + 10 files changed, 137 insertions(+), 18 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/MenuInvalidator.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/history/domain/model/HistoryOrder.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 2b04b7d27..7e9b197e6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -23,6 +23,7 @@ import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.putEnumValue import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.util.ext.toUriOrNull +import org.koitharu.kotatsu.history.domain.model.HistoryOrder import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.util.mapNotNullToSet import org.koitharu.kotatsu.parsers.util.mapToSet @@ -272,6 +273,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST) set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) } + var historySortOrder: HistoryOrder + get() = prefs.getEnumValue(KEY_HISTORY_ORDER, HistoryOrder.UPDATED) + set(value) = prefs.edit { putEnumValue(KEY_HISTORY_ORDER, value) } + val isWebtoonZoomEnable: Boolean get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true) @@ -418,6 +423,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_SHORTCUTS = "dynamic_shortcuts" const val KEY_READER_TAPS_LTR = "reader_taps_ltr" const val KEY_LOCAL_LIST_ORDER = "local_order" + const val KEY_HISTORY_ORDER = "history_order" const val KEY_WEBTOON_ZOOM = "webtoon_zoom" const val KEY_PREFETCH_CONTENT = "prefetch_content" const val KEY_APP_LOCALE = "app_locale" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/MenuInvalidator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/MenuInvalidator.kt new file mode 100644 index 000000000..2a184b63d --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/MenuInvalidator.kt @@ -0,0 +1,13 @@ +package org.koitharu.kotatsu.core.ui.util + +import androidx.fragment.app.Fragment +import kotlinx.coroutines.flow.FlowCollector + +class MenuInvalidator( + private val fragment: Fragment, +) : FlowCollector { + + override suspend fun emit(value: Any) { + fragment.activity?.invalidateMenu() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt index 0e033ddd5..cc51633e6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt @@ -4,10 +4,15 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.RawQuery import androidx.room.Transaction +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery import kotlinx.coroutines.flow.Flow +import org.intellij.lang.annotations.Language import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.TagEntity +import org.koitharu.kotatsu.history.domain.model.HistoryOrder @Dao abstract class HistoryDao { @@ -28,6 +33,22 @@ abstract class HistoryDao { @Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit") abstract fun observeAll(limit: Int): Flow> + fun observeAll(order: HistoryOrder): Flow> { + val orderBy = when (order) { + HistoryOrder.UPDATED -> "history.updated_at DESC" + HistoryOrder.CREATED -> "history.created_at DESC" + HistoryOrder.PROGRESS -> "history.percent DESC" + HistoryOrder.ALPHABETIC -> "manga.title" + } + + @Language("RoomSql") + val query = SimpleSQLiteQuery( + "SELECT * FROM history LEFT JOIN manga ON history.manga_id = manga.manga_id " + + "WHERE history.deleted_at = 0 GROUP BY history.manga_id ORDER BY $orderBy", + ) + return observeAllImpl(query) + } + @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history WHERE deleted_at = 0)") abstract suspend fun findAllManga(): List @@ -111,4 +132,8 @@ abstract class HistoryDao { @Query("UPDATE history SET deleted_at = :deletedAt WHERE created_at >= :minDate AND deleted_at = 0") protected abstract suspend fun setDeletedAtAfter(minDate: Long, deletedAt: Long) + + @Transaction + @RawQuery(observedEntities = [HistoryEntity::class]) + protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow> } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt index 28f6bf451..dce60aa30 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt @@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.ui.util.ReversibleHandle import org.koitharu.kotatsu.core.util.ext.mapItems +import org.koitharu.kotatsu.history.domain.model.HistoryOrder import org.koitharu.kotatsu.history.domain.model.MangaWithHistory import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag @@ -64,8 +65,8 @@ class HistoryRepository @Inject constructor( } } - fun observeAllWithHistory(): Flow> { - return db.historyDao.observeAll().mapItems { + fun observeAllWithHistory(order: HistoryOrder): Flow> { + return db.historyDao.observeAll(order).mapItems { MangaWithHistory( it.manga.toManga(it.tags.toMangaTags()), it.history.toMangaHistory(), diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/model/HistoryOrder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/model/HistoryOrder.kt new file mode 100644 index 000000000..215c59c1f --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/model/HistoryOrder.kt @@ -0,0 +1,16 @@ +package org.koitharu.kotatsu.history.domain.model + +import androidx.annotation.StringRes +import org.koitharu.kotatsu.R + +enum class HistoryOrder( + @StringRes val titleResId: Int, +) { + + UPDATED(R.string.updated), + CREATED(R.string.order_added), + PROGRESS(R.string.progress), + ALPHABETIC(R.string.by_name); + + fun isGroupingSupported() = this == UPDATED || this == CREATED +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt index f50af0f3e..006a64a30 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt @@ -8,6 +8,7 @@ import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.ui.util.MenuInvalidator import org.koitharu.kotatsu.core.util.ext.addMenuProvider import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.databinding.FragmentListBinding @@ -23,9 +24,9 @@ class HistoryListFragment : MangaListFragment() { override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { super.onViewBindingCreated(binding, savedInstanceState) addMenuProvider(HistoryListMenuProvider(binding.root.context, viewModel)) - viewModel.isGroupingEnabled.observe(viewLifecycleOwner) { - activity?.invalidateOptionsMenu() - } + val menuInvalidator = MenuInvalidator(this) + viewModel.isGroupingEnabled.observe(viewLifecycleOwner, menuInvalidator) + viewModel.sortOrder.observe(viewLifecycleOwner, menuInvalidator) } override fun onScrolledToEnd() = Unit diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt index 32b0e6f03..978bc0b12 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt @@ -5,10 +5,12 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import androidx.core.view.MenuProvider +import androidx.core.view.forEach import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ui.dialog.RememberSelectionDialogListener import org.koitharu.kotatsu.core.util.ext.startOfDay +import org.koitharu.kotatsu.history.domain.model.HistoryOrder import java.util.Date import java.util.concurrent.TimeUnit import com.google.android.material.R as materialR @@ -20,24 +22,45 @@ class HistoryListMenuProvider( override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.opt_history, menu) + val subMenu = menu.findItem(R.id.action_order)?.subMenu ?: return + for (order in HistoryOrder.values()) { + subMenu.add(R.id.group_order, Menu.NONE, order.ordinal, order.titleResId) + } + subMenu.setGroupCheckable(R.id.group_order, true, true) } - override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { - R.id.action_clear_history -> { - showClearHistoryDialog() - true + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + if (menuItem.groupId == R.id.group_order) { + val order = enumValues()[menuItem.order] + viewModel.setSortOrder(order) + return true } + return when (menuItem.itemId) { + R.id.action_clear_history -> { + showClearHistoryDialog() + true + } - R.id.action_history_grouping -> { - viewModel.setGrouping(!menuItem.isChecked) - true - } + R.id.action_history_grouping -> { + viewModel.setGrouping(!menuItem.isChecked) + true + } - else -> false + else -> false + } } override fun onPrepareMenu(menu: Menu) { - menu.findItem(R.id.action_history_grouping)?.isChecked = viewModel.isGroupingEnabled.value == true + val order = viewModel.sortOrder.value ?: return + menu.findItem(R.id.action_order)?.subMenu?.forEach { item -> + if (item.order == order.ordinal) { + item.isChecked = true + } + } + menu.findItem(R.id.action_history_grouping)?.run { + isChecked = viewModel.isGroupingEnabled.value == true + isEnabled = order.isGroupingSupported() + } } private fun showClearHistoryDialog() { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index bc394e3f7..3b041f9d5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -4,14 +4,17 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.ui.model.DateTimeAgo import org.koitharu.kotatsu.core.ui.util.ReversibleAction @@ -20,6 +23,7 @@ import org.koitharu.kotatsu.core.util.ext.daysDiff import org.koitharu.kotatsu.core.util.ext.onFirst import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.history.data.HistoryRepository +import org.koitharu.kotatsu.history.domain.model.HistoryOrder import org.koitharu.kotatsu.history.domain.model.MangaWithHistory import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.ui.MangaListViewModel @@ -43,14 +47,25 @@ class HistoryListViewModel @Inject constructor( downloadScheduler: DownloadWorker.Scheduler, ) : MangaListViewModel(settings, downloadScheduler) { - val isGroupingEnabled = settings.observeAsStateFlow( - scope = viewModelScope + Dispatchers.Default, + val sortOrder: StateFlow = settings.observeAsStateFlow( + scope = viewModelScope + Dispatchers.IO, + key = AppSettings.KEY_HISTORY_ORDER, + valueProducer = { historySortOrder }, + ) + + val isGroupingEnabled = settings.observeAsFlow( key = AppSettings.KEY_HISTORY_GROUPING, valueProducer = { isHistoryGroupingEnabled }, + ).combine(sortOrder) { g, s -> + g && s.isGroupingSupported() + }.stateIn( + scope = viewModelScope + Dispatchers.Default, + started = SharingStarted.Eagerly, + initialValue = settings.isHistoryGroupingEnabled && sortOrder.value.isGroupingSupported(), ) override val content = combine( - repository.observeAllWithHistory(), + sortOrder.flatMapLatest { repository.observeAllWithHistory(it) }, isGroupingEnabled, listMode, ) { list, grouped, mode -> @@ -78,6 +93,10 @@ class HistoryListViewModel @Inject constructor( override fun onRetry() = Unit + fun setSortOrder(order: HistoryOrder) { + settings.historySortOrder = order + } + fun clearHistory(minDate: Long) { launchJob(Dispatchers.Default) { val stringRes = if (minDate <= 0) { diff --git a/app/src/main/res/menu/opt_history.xml b/app/src/main/res/menu/opt_history.xml index 0c8d45d4e..3f3a973af 100644 --- a/app/src/main/res/menu/opt_history.xml +++ b/app/src/main/res/menu/opt_history.xml @@ -3,6 +3,19 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + + + + + + + Do not update suggestions using metered network connections Do not check for new chapters using metered network connections Enter manga title, genre or source name + Progress + Added