#8 Configure sort order for each favourites category

This commit is contained in:
Koitharu
2021-08-04 08:32:20 +03:00
parent 6f7efa9e26
commit fbd0f25b8f
19 changed files with 201 additions and 45 deletions

View File

@@ -118,6 +118,7 @@ class BackupRepository(private val db: MangaDatabase) {
jo.put("created_at", createdAt)
jo.put("sort_key", sortKey)
jo.put("title", title)
jo.put("order", order)
return jo
}

View File

@@ -5,6 +5,7 @@ import org.json.JSONObject
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
@@ -101,7 +102,8 @@ class RestoreRepository(private val db: MangaDatabase) {
categoryId = json.getInt("category_id"),
createdAt = json.getLong("created_at"),
sortKey = json.getInt("sort_key"),
title = json.getString("title")
title = json.getString("title"),
order = json.getStringOrNull("order") ?: SortOrder.NEWEST.name,
)
private fun parseFavourite(json: JSONObject) = FavouriteEntity(

View File

@@ -20,6 +20,7 @@ val databaseModule
Migration5To6(),
Migration6To7(),
Migration7To8(),
Migration8To9(),
).addCallback(
DatabasePrePopulateCallback(androidContext().resources)
).build()

View File

@@ -16,7 +16,7 @@ import org.koitharu.kotatsu.history.data.HistoryEntity
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class
], version = 8
], version = 9
)
abstract class MangaDatabase : RoomDatabase() {

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import org.koitharu.kotatsu.core.model.SortOrder
class Migration8To9 : Migration(8, 9) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `order` TEXT NOT NULL DEFAULT ${SortOrder.NEWEST.name}")
}
}

View File

@@ -9,5 +9,6 @@ data class FavouriteCategory(
val id: Long,
val title: String,
val sortKey: Int,
val createdAt: Date
val order: SortOrder,
val createdAt: Date,
) : Parcelable

View File

@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.favourites.data
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.model.FavouriteCategory
@Dao
abstract class FavouriteCategoriesDao {
@@ -13,6 +12,9 @@ abstract class FavouriteCategoriesDao {
@Query("SELECT * FROM favourite_categories ORDER BY sort_key")
abstract fun observeAll(): Flow<List<FavouriteCategoryEntity>>
@Query("SELECT * FROM favourite_categories WHERE category_id = :id")
abstract fun observe(id: Long): Flow<FavouriteCategoryEntity>
@Insert(onConflict = OnConflictStrategy.ABORT)
abstract suspend fun insert(category: FavouriteCategoryEntity): Long
@@ -23,10 +25,13 @@ abstract class FavouriteCategoriesDao {
abstract suspend fun delete(id: Long)
@Query("UPDATE favourite_categories SET title = :title WHERE category_id = :id")
abstract suspend fun update(id: Long, title: String)
abstract suspend fun updateTitle(id: Long, title: String)
@Query("UPDATE favourite_categories SET `order` = :order WHERE category_id = :id")
abstract suspend fun updateOrder(id: Long, order: String)
@Query("UPDATE favourite_categories SET sort_key = :sortKey WHERE category_id = :id")
abstract suspend fun update(id: Long, sortKey: Int)
abstract suspend fun updateSortKey(id: Long, sortKey: Int)
@Query("SELECT MAX(sort_key) FROM favourite_categories")
protected abstract suspend fun getMaxSortKey(): Int?

View File

@@ -4,6 +4,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.SortOrder
import java.util.*
@Entity(tableName = "favourite_categories")
@@ -12,13 +13,15 @@ data class FavouriteCategoryEntity(
@ColumnInfo(name = "category_id") val categoryId: Int,
@ColumnInfo(name = "created_at") val createdAt: Long,
@ColumnInfo(name = "sort_key") val sortKey: Int,
@ColumnInfo(name = "title") val title: String
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "order") val order: String,
) {
fun toFavouriteCategory(id: Long? = null) = FavouriteCategory(
id = id ?: categoryId.toLong(),
title = title,
sortKey = sortKey,
createdAt = Date(createdAt)
order = SortOrder.values().find { x -> x.name == order } ?: SortOrder.NEWEST,
createdAt = Date(createdAt),
)
}

View File

@@ -1,8 +1,11 @@
package org.koitharu.kotatsu.favourites.data
import androidx.room.*
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.model.SortOrder
@Dao
abstract class FavouritesDao {
@@ -11,9 +14,13 @@ abstract class FavouritesDao {
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at DESC")
abstract suspend fun findAll(): List<FavouriteManga>
@Transaction
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at DESC")
abstract fun observeAll(): Flow<List<FavouriteManga>>
fun observeAll(order: SortOrder): Flow<List<FavouriteManga>> {
val orderBy = getOrderBy(order)
val query = SimpleSQLiteQuery(
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id GROUP BY favourites.manga_id ORDER BY $orderBy",
)
return observeAllRaw(query)
}
@Transaction
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
@@ -23,9 +30,14 @@ abstract class FavouritesDao {
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at DESC")
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga>
@Transaction
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at DESC")
abstract fun observeAll(categoryId: Long): Flow<List<FavouriteManga>>
fun observeAll(categoryId: Long, order: SortOrder): Flow<List<FavouriteManga>> {
val orderBy = getOrderBy(order)
val query = SimpleSQLiteQuery(
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id WHERE category_id = ? GROUP BY favourites.manga_id ORDER BY $orderBy",
arrayOf<Any>(categoryId),
)
return observeAllRaw(query)
}
@Transaction
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
@@ -63,4 +75,16 @@ abstract class FavouritesDao {
insert(entity)
}
}
@Transaction
@RawQuery(observedEntities = [FavouriteEntity::class])
protected abstract fun observeAllRaw(query: SupportSQLiteQuery): Flow<List<FavouriteManga>>
private fun getOrderBy(sortOrder: SortOrder) = when(sortOrder) {
SortOrder.RATING -> "rating DESC"
SortOrder.NEWEST,
SortOrder.UPDATED -> "created_at DESC"
SortOrder.ALPHABETICAL -> "title ASC"
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
}
}

View File

@@ -3,12 +3,14 @@ package org.koitharu.kotatsu.favourites.domain
import androidx.room.withTransaction
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.utils.ext.mapItems
@@ -21,26 +23,26 @@ class FavouritesRepository(private val db: MangaDatabase) {
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
}
fun observeAll(): Flow<List<Manga>> {
return db.favouritesDao.observeAll()
fun observeAll(order: SortOrder): Flow<List<Manga>> {
return db.favouritesDao.observeAll(order)
.mapItems { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
}
suspend fun getAllManga(offset: Int): List<Manga> {
val entities = db.favouritesDao.findAll(offset, 20)
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
}
suspend fun getManga(categoryId: Long): List<Manga> {
val entities = db.favouritesDao.findAll(categoryId)
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
}
fun observeAll(categoryId: Long): Flow<List<Manga>> {
return db.favouritesDao.observeAll(categoryId)
fun observeAll(categoryId: Long, order: SortOrder): Flow<List<Manga>> {
return db.favouritesDao.observeAll(categoryId, order)
.mapItems { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
}
fun observeAll(categoryId: Long): Flow<List<Manga>> {
return observeOrder(categoryId)
.flatMapLatest { order -> observeAll(categoryId, order) }
}
suspend fun getManga(categoryId: Long, offset: Int): List<Manga> {
val entities = db.favouritesDao.findAll(categoryId, offset, 20)
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
@@ -77,25 +79,30 @@ class FavouritesRepository(private val db: MangaDatabase) {
title = title,
createdAt = System.currentTimeMillis(),
sortKey = db.favouriteCategoriesDao.getNextSortKey(),
categoryId = 0
categoryId = 0,
order = SortOrder.UPDATED.name,
)
val id = db.favouriteCategoriesDao.insert(entity)
return entity.toFavouriteCategory(id)
}
suspend fun renameCategory(id: Long, title: String) {
db.favouriteCategoriesDao.update(id, title)
db.favouriteCategoriesDao.updateTitle(id, title)
}
suspend fun removeCategory(id: Long) {
db.favouriteCategoriesDao.delete(id)
}
suspend fun setCategoryOrder(id: Long, order: SortOrder) {
db.favouriteCategoriesDao.updateOrder(id, order.name)
}
suspend fun reorderCategories(orderedIds: List<Long>) {
val dao = db.favouriteCategoriesDao
db.withTransaction {
for ((i, id) in orderedIds.withIndex()) {
dao.update(id, i)
dao.updateSortKey(id, i)
}
}
}
@@ -117,4 +124,10 @@ class FavouritesRepository(private val db: MangaDatabase) {
suspend fun removeFromFavourites(manga: Manga) {
db.favouritesDao.delete(manga.id)
}
private fun observeOrder(categoryId: Long): Flow<SortOrder> {
return db.favouriteCategoriesDao.observe(categoryId)
.map { x -> SortOrder.values().find { it.name == x.order } ?: SortOrder.NEWEST }
.distinctUntilChanged()
}
}

View File

@@ -11,6 +11,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
@@ -19,7 +20,6 @@ import org.koitharu.kotatsu.utils.RecycledViewPoolHolder
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.showPopupMenu
import java.util.*
import kotlin.collections.ArrayList
class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
FavouritesTabLongClickListener, CategoriesEditDelegate.CategoriesEditCallback,
@@ -100,11 +100,19 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
override fun onTabLongClick(tabView: View, category: FavouriteCategory): Boolean {
val menuRes = if (category.id == 0L) R.menu.popup_category_empty else R.menu.popup_category
tabView.showPopupMenu(menuRes) {
tabView.showPopupMenu(menuRes, { menu ->
createOrderSubmenu(menu, category)
}) {
when (it.itemId) {
R.id.action_remove -> editDelegate.deleteCategory(category)
R.id.action_rename -> editDelegate.renameCategory(category)
R.id.action_create -> editDelegate.createCategory()
R.id.action_order -> return@showPopupMenu false
else -> {
val order = CategoriesActivity.SORT_ORDERS.getOrNull(it.order)
?: return@showPopupMenu false
viewModel.setCategoryOrder(category.id, order)
}
}
true
}
@@ -125,11 +133,26 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
private fun wrapCategories(categories: List<FavouriteCategory>): List<FavouriteCategory> {
val data = ArrayList<FavouriteCategory>(categories.size + 1)
data += FavouriteCategory(0L, getString(R.string.all_favourites), -1, Date())
data += FavouriteCategory(0L, getString(R.string.all_favourites), -1, SortOrder.NEWEST, Date())
data += categories
return data
}
private fun createOrderSubmenu(menu: Menu, category: FavouriteCategory) {
val submenu = menu.findItem(R.id.action_order)?.subMenu ?: return
for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) {
val menuItem = submenu.add(
R.id.group_order,
Menu.NONE,
i,
item.titleRes
)
menuItem.isCheckable = true
menuItem.isChecked = item == category.order
}
submenu.setGroupCheckable(R.id.group_order, true, true)
}
companion object {
fun newInstance() = FavouritesContainerFragment()

View File

@@ -36,7 +36,7 @@ class FavouritesPagerAdapter(
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
val item = differ.currentList[position]
tab.text = item.title
tab.view.tag = item
tab.view.tag = item.id
tab.view.setOnLongClickListener(this)
}
@@ -45,7 +45,8 @@ class FavouritesPagerAdapter(
}
override fun onLongClick(v: View): Boolean {
val item = v.tag as? FavouriteCategory ?: return false
val itemId = v.tag as? Long ?: return false
val item = differ.currentList.find { x -> x.id == itemId } ?: return false
return longClickListener.onTabLongClick(v, item)
}

View File

@@ -5,6 +5,7 @@ import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.Insets
@@ -20,6 +21,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.showPopupMenu
@@ -61,10 +63,17 @@ class CategoriesActivity : BaseActivity<ActivityCategoriesBinding>(),
}
override fun onItemClick(item: FavouriteCategory, view: View) {
view.showPopupMenu(R.menu.popup_category) {
view.showPopupMenu(R.menu.popup_category, { menu ->
createOrderSubmenu(menu, item)
}) {
when (it.itemId) {
R.id.action_remove -> editDelegate.deleteCategory(item)
R.id.action_rename -> editDelegate.renameCategory(item)
R.id.action_order -> return@showPopupMenu false
else -> {
val order = SORT_ORDERS.getOrNull(it.order) ?: return@showPopupMenu false
viewModel.setCategoryOrder(item.id, order)
}
}
true
}
@@ -117,6 +126,21 @@ class CategoriesActivity : BaseActivity<ActivityCategoriesBinding>(),
viewModel.createCategory(name)
}
private fun createOrderSubmenu(menu: Menu, category: FavouriteCategory) {
val submenu = menu.findItem(R.id.action_order)?.subMenu ?: return
for ((i, item) in SORT_ORDERS.withIndex()) {
val menuItem = submenu.add(
R.id.group_order,
Menu.NONE,
i,
item.titleRes
)
menuItem.isCheckable = true
menuItem.isChecked = item == category.order
}
submenu.setGroupCheckable(R.id.group_order, true, true)
}
private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.DOWN or ItemTouchHelper.UP, 0
) {
@@ -145,6 +169,12 @@ class CategoriesActivity : BaseActivity<ActivityCategoriesBinding>(),
companion object {
val SORT_ORDERS = arrayOf(
SortOrder.ALPHABETICAL,
SortOrder.NEWEST,
SortOrder.RATING,
)
fun newIntent(context: Context) = Intent(context, CategoriesActivity::class.java)
}
}

View File

@@ -6,7 +6,7 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
class CategoriesAdapter(
onItemClickListener: OnListItemClickListener<FavouriteCategory>
onItemClickListener: OnListItemClickListener<FavouriteCategory>,
) : AsyncListDifferDelegationAdapter<FavouriteCategory>(DiffCallback()) {
init {
@@ -20,12 +20,27 @@ class CategoriesAdapter(
private class DiffCallback : DiffUtil.ItemCallback<FavouriteCategory>() {
override fun areItemsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
override fun areItemsTheSame(
oldItem: FavouriteCategory,
newItem: FavouriteCategory,
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
override fun areContentsTheSame(
oldItem: FavouriteCategory,
newItem: FavouriteCategory,
): Boolean {
return oldItem.id == newItem.id && oldItem.title == newItem.title
&& oldItem.order == newItem.order
}
override fun getChangePayload(
oldItem: FavouriteCategory,
newItem: FavouriteCategory,
): Any? = when {
oldItem.title == newItem.title && oldItem.order != newItem.order -> newItem.order
else -> super.getChangePayload(oldItem, newItem)
}
}
}

View File

@@ -4,10 +4,10 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import java.util.*
import kotlin.collections.ArrayList
class FavouritesCategoriesViewModel(
private val repository: FavouritesRepository
@@ -19,23 +19,29 @@ class FavouritesCategoriesViewModel(
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
fun createCategory(name: String) {
launchJob(Dispatchers.Default) {
launchJob {
repository.addCategory(name)
}
}
fun renameCategory(id: Long, name: String) {
launchJob(Dispatchers.Default) {
launchJob {
repository.renameCategory(id, name)
}
}
fun deleteCategory(id: Long) {
launchJob(Dispatchers.Default) {
launchJob {
repository.removeCategory(id)
}
}
fun setCategoryOrder(id: Long, order: SortOrder) {
launchJob {
repository.setCategoryOrder(id, order)
}
}
fun reorderCategories(oldPos: Int, newPos: Int) {
val prevJob = reorderJob
reorderJob = launchJob(Dispatchers.Default) {

View File

@@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.list.ui.MangaListViewModel
@@ -22,7 +23,11 @@ class FavouritesListViewModel(
) : MangaListViewModel(settings) {
override val content = combine(
if (categoryId == 0L) repository.observeAll() else repository.observeAll(categoryId),
if (categoryId == 0L) {
repository.observeAll(SortOrder.NEWEST)
} else {
repository.observeAll(categoryId)
},
createListModeFlow()
) { list, mode ->
when {

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.retry
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
@@ -17,7 +18,7 @@ import org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider
class WidgetUpdater(private val context: Context) {
fun subscribeToFavourites(repository: FavouritesRepository) {
repository.observeAll()
repository.observeAll(SortOrder.NEWEST)
.onEach { updateWidget(ShelfWidgetProvider::class.java) }
.retry { error -> error !is CancellationException }
.launchIn(processLifecycleScope)

View File

@@ -10,4 +10,17 @@
android:id="@+id/action_rename"
android:title="@string/rename" />
<item
android:id="@+id/action_order"
android:title="@string/sort_order">
<menu>
<group
android:id="@+id/group_order"
android:checkableBehavior="single" />
</menu>
</item>
</menu>