History sorting #428

pull/440/head
Koitharu 3 years ago
parent ac1a919476
commit 7c7106a63c
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -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.putEnumValue
import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.util.ext.takeIfReadable
import org.koitharu.kotatsu.core.util.ext.toUriOrNull 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.model.SortOrder
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.mapToSet 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) get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) } 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 val isWebtoonZoomEnable: Boolean
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true) 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_SHORTCUTS = "dynamic_shortcuts"
const val KEY_READER_TAPS_LTR = "reader_taps_ltr" const val KEY_READER_TAPS_LTR = "reader_taps_ltr"
const val KEY_LOCAL_LIST_ORDER = "local_order" 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_WEBTOON_ZOOM = "webtoon_zoom"
const val KEY_PREFETCH_CONTENT = "prefetch_content" const val KEY_PREFETCH_CONTENT = "prefetch_content"
const val KEY_APP_LOCALE = "app_locale" const val KEY_APP_LOCALE = "app_locale"

@ -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<Any> {
override suspend fun emit(value: Any) {
fragment.activity?.invalidateMenu()
}
}

@ -4,10 +4,15 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction import androidx.room.Transaction
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow 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.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.history.domain.model.HistoryOrder
@Dao @Dao
abstract class HistoryDao { 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") @Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit")
abstract fun observeAll(limit: Int): Flow<List<HistoryWithManga>> abstract fun observeAll(limit: Int): Flow<List<HistoryWithManga>>
fun observeAll(order: HistoryOrder): Flow<List<HistoryWithManga>> {
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)") @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history WHERE deleted_at = 0)")
abstract suspend fun findAllManga(): List<MangaEntity> abstract suspend fun findAllManga(): List<MangaEntity>
@ -111,4 +132,8 @@ abstract class HistoryDao {
@Query("UPDATE history SET deleted_at = :deletedAt WHERE created_at >= :minDate AND deleted_at = 0") @Query("UPDATE history SET deleted_at = :deletedAt WHERE created_at >= :minDate AND deleted_at = 0")
protected abstract suspend fun setDeletedAtAfter(minDate: Long, deletedAt: Long) protected abstract suspend fun setDeletedAtAfter(minDate: Long, deletedAt: Long)
@Transaction
@RawQuery(observedEntities = [HistoryEntity::class])
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<HistoryWithManga>>
} }

@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.core.util.ext.mapItems 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.history.domain.model.MangaWithHistory
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
@ -64,8 +65,8 @@ class HistoryRepository @Inject constructor(
} }
} }
fun observeAllWithHistory(): Flow<List<MangaWithHistory>> { fun observeAllWithHistory(order: HistoryOrder): Flow<List<MangaWithHistory>> {
return db.historyDao.observeAll().mapItems { return db.historyDao.observeAll(order).mapItems {
MangaWithHistory( MangaWithHistory(
it.manga.toManga(it.tags.toMangaTags()), it.manga.toManga(it.tags.toMangaTags()),
it.history.toMangaHistory(), it.history.toMangaHistory(),

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

@ -8,6 +8,7 @@ import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.ListSelectionController 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.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.databinding.FragmentListBinding
@ -23,9 +24,9 @@ class HistoryListFragment : MangaListFragment() {
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
addMenuProvider(HistoryListMenuProvider(binding.root.context, viewModel)) addMenuProvider(HistoryListMenuProvider(binding.root.context, viewModel))
viewModel.isGroupingEnabled.observe(viewLifecycleOwner) { val menuInvalidator = MenuInvalidator(this)
activity?.invalidateOptionsMenu() viewModel.isGroupingEnabled.observe(viewLifecycleOwner, menuInvalidator)
} viewModel.sortOrder.observe(viewLifecycleOwner, menuInvalidator)
} }
override fun onScrolledToEnd() = Unit override fun onScrolledToEnd() = Unit

@ -5,10 +5,12 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import androidx.core.view.forEach
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.dialog.RememberSelectionDialogListener import org.koitharu.kotatsu.core.ui.dialog.RememberSelectionDialogListener
import org.koitharu.kotatsu.core.util.ext.startOfDay import org.koitharu.kotatsu.core.util.ext.startOfDay
import org.koitharu.kotatsu.history.domain.model.HistoryOrder
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@ -20,9 +22,20 @@ class HistoryListMenuProvider(
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_history, menu) 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) { override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
if (menuItem.groupId == R.id.group_order) {
val order = enumValues<HistoryOrder>()[menuItem.order]
viewModel.setSortOrder(order)
return true
}
return when (menuItem.itemId) {
R.id.action_clear_history -> { R.id.action_clear_history -> {
showClearHistoryDialog() showClearHistoryDialog()
true true
@ -35,9 +48,19 @@ class HistoryListMenuProvider(
else -> false else -> false
} }
}
override fun onPrepareMenu(menu: Menu) { 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() { private fun showClearHistoryDialog() {

@ -4,14 +4,17 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
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.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.ui.util.ReversibleAction 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.core.util.ext.onFirst
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.data.HistoryRepository 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.history.domain.model.MangaWithHistory
import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
@ -43,14 +47,25 @@ class HistoryListViewModel @Inject constructor(
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) { ) : MangaListViewModel(settings, downloadScheduler) {
val isGroupingEnabled = settings.observeAsStateFlow( val sortOrder: StateFlow<HistoryOrder> = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default, scope = viewModelScope + Dispatchers.IO,
key = AppSettings.KEY_HISTORY_ORDER,
valueProducer = { historySortOrder },
)
val isGroupingEnabled = settings.observeAsFlow(
key = AppSettings.KEY_HISTORY_GROUPING, key = AppSettings.KEY_HISTORY_GROUPING,
valueProducer = { isHistoryGroupingEnabled }, 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( override val content = combine(
repository.observeAllWithHistory(), sortOrder.flatMapLatest { repository.observeAllWithHistory(it) },
isGroupingEnabled, isGroupingEnabled,
listMode, listMode,
) { list, grouped, mode -> ) { list, grouped, mode ->
@ -78,6 +93,10 @@ class HistoryListViewModel @Inject constructor(
override fun onRetry() = Unit override fun onRetry() = Unit
fun setSortOrder(order: HistoryOrder) {
settings.historySortOrder = order
}
fun clearHistory(minDate: Long) { fun clearHistory(minDate: Long) {
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val stringRes = if (minDate <= 0) { val stringRes = if (minDate <= 0) {

@ -3,6 +3,19 @@
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_order"
android:orderInCategory="25"
android:title="@string/sort_order">
<menu>
<group android:id="@+id/group_order" />
</menu>
</item>
<item <item
android:id="@+id/action_history_grouping" android:id="@+id/action_history_grouping"
android:checkable="true" android:checkable="true"

@ -464,4 +464,6 @@
<string name="suggestions_wifi_only_summary">Do not update suggestions using metered network connections</string> <string name="suggestions_wifi_only_summary">Do not update suggestions using metered network connections</string>
<string name="tracker_wifi_only_summary">Do not check for new chapters using metered network connections</string> <string name="tracker_wifi_only_summary">Do not check for new chapters using metered network connections</string>
<string name="search_hint">Enter manga title, genre or source name</string> <string name="search_hint">Enter manga title, genre or source name</string>
<string name="progress">Progress</string>
<string name="order_added">Added</string>
</resources> </resources>

Loading…
Cancel
Save