Configure visible categories in library

This commit is contained in:
Koitharu
2022-07-12 12:48:50 +03:00
parent e2aea345d4
commit 2654de96ba
22 changed files with 318 additions and 30 deletions

View File

@@ -123,6 +123,7 @@ class BackupRepository(private val db: MangaDatabase) {
jo.put("title", title)
jo.put("order", order)
jo.put("track", track)
jo.put("show_in_lib", isVisibleInLibrary)
return jo
}
@@ -131,6 +132,7 @@ class BackupRepository(private val db: MangaDatabase) {
jo.put("manga_id", mangaId)
jo.put("category_id", categoryId)
jo.put("created_at", createdAt)
jo.put("sort_key", sortKey)
return jo
}
}

View File

@@ -103,11 +103,13 @@ class RestoreRepository(private val db: MangaDatabase) {
title = json.getString("title"),
order = json.getStringOrNull("order") ?: SortOrder.NEWEST.name,
track = json.getBooleanOrDefault("track", true),
isVisibleInLibrary = json.getBooleanOrDefault("show_in_lib", true),
)
private fun parseFavourite(json: JSONObject) = FavouriteEntity(
mangaId = json.getLong("manga_id"),
categoryId = json.getLong("category_id"),
createdAt = json.getLong("created_at")
createdAt = json.getLong("created_at"),
sortKey = json.getIntOrDefault("sort_key", 0),
)
}

View File

@@ -36,7 +36,7 @@ import org.koitharu.kotatsu.tracker.data.TracksDao
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class,
ScrobblingEntity::class,
],
version = 12,
version = 13,
)
abstract class MangaDatabase : RoomDatabase() {
@@ -79,6 +79,7 @@ fun MangaDatabase(context: Context): MangaDatabase = Room.databaseBuilder(
Migration9To10(),
Migration10To11(),
Migration11To12(),
Migration12To13(),
).addCallback(
DatabasePrePopulateCallback(context.resources)
).build()

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration12To13 : Migration(12, 13) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `show_in_lib` INTEGER NOT NULL DEFAULT 1")
database.execSQL("ALTER TABLE favourites ADD COLUMN `sort_key` INTEGER NOT NULL DEFAULT 0")
}
}

View File

@@ -1,9 +1,9 @@
package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import java.util.*
import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.parsers.model.SortOrder
import java.util.*
@Parcelize
data class FavouriteCategory(
@@ -13,4 +13,5 @@ data class FavouriteCategory(
val order: SortOrder,
val createdAt: Date,
val isTrackingEnabled: Boolean,
val isVisibleInLibrary: Boolean,
) : Parcelable

View File

@@ -12,4 +12,5 @@ fun FavouriteCategoryEntity.toFavouriteCategory(id: Long = categoryId.toLong())
order = SortOrder(order, SortOrder.NEWEST),
createdAt = Date(createdAt),
isTrackingEnabled = track,
isVisibleInLibrary = isVisibleInLibrary,
)

View File

@@ -50,6 +50,9 @@ abstract class FavouriteCategoriesDao {
@Query("UPDATE favourite_categories SET `track` = :isEnabled WHERE category_id = :id")
abstract suspend fun updateTracking(id: Long, isEnabled: Boolean)
@Query("UPDATE favourite_categories SET `show_in_lib` = :isEnabled WHERE category_id = :id")
abstract suspend fun updateLibVisibility(id: Long, isEnabled: Boolean)
@Query("UPDATE favourite_categories SET sort_key = :sortKey WHERE category_id = :id")
abstract suspend fun updateSortKey(id: Long, sortKey: Int)

View File

@@ -13,6 +13,7 @@ class FavouriteCategoryEntity(
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "order") val order: String,
@ColumnInfo(name = "track") val track: Boolean,
@ColumnInfo(name = "show_in_lib") val isVisibleInLibrary: Boolean,
) {
override fun equals(other: Any?): Boolean {
@@ -27,6 +28,7 @@ class FavouriteCategoryEntity(
if (title != other.title) return false
if (order != other.order) return false
if (track != other.track) return false
if (isVisibleInLibrary != other.isVisibleInLibrary) return false
return true
}
@@ -38,6 +40,7 @@ class FavouriteCategoryEntity(
result = 31 * result + title.hashCode()
result = 31 * result + order.hashCode()
result = 31 * result + track.hashCode()
result = 31 * result + isVisibleInLibrary.hashCode()
return result
}
}

View File

@@ -24,5 +24,29 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
class FavouriteEntity(
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "category_id", index = true) val categoryId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long
)
@ColumnInfo(name = "sort_key") val sortKey: Int,
@ColumnInfo(name = "created_at") val createdAt: Long,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FavouriteEntity
if (mangaId != other.mangaId) return false
if (categoryId != other.categoryId) return false
if (sortKey != other.sortKey) return false
if (createdAt != other.createdAt) return false
return true
}
override fun hashCode(): Int {
var result = mangaId.hashCode()
result = 31 * result + categoryId.hashCode()
result = 31 * result + sortKey
result = 31 * result + createdAt.hashCode()
return result
}
}

View File

@@ -29,11 +29,6 @@ class FavouritesRepository(
.mapItems { it.manga.toManga(it.tags.toMangaTags()) }
}
fun observeAllGrouped(order: SortOrder): Flow<Map<FavouriteCategory, List<Manga>>> {
return db.favouritesDao.observeAll(order)
.map { list -> groupByCategory(list) }
}
suspend fun getManga(categoryId: Long): List<Manga> {
val entities = db.favouritesDao.findAll(categoryId)
return entities.map { it.manga.toManga(it.tags.toMangaTags()) }
@@ -89,6 +84,7 @@ class FavouritesRepository(
categoryId = 0,
order = sortOrder.name,
track = isTrackerEnabled,
isVisibleInLibrary = true,
)
val id = db.favouriteCategoriesDao.insert(entity)
val category = entity.toFavouriteCategory(id)
@@ -100,6 +96,10 @@ class FavouritesRepository(
db.favouriteCategoriesDao.update(id, title, sortOrder.name, isTrackerEnabled)
}
suspend fun updateCategory(id: Long, isVisibleInLibrary: Boolean) {
db.favouriteCategoriesDao.updateLibVisibility(id, isVisibleInLibrary)
}
suspend fun addCategory(title: String): FavouriteCategory {
val entity = FavouriteCategoryEntity(
title = title,
@@ -108,6 +108,7 @@ class FavouritesRepository(
categoryId = 0,
order = SortOrder.NEWEST.name,
track = true,
isVisibleInLibrary = true,
)
val id = db.favouriteCategoriesDao.insert(entity)
val category = entity.toFavouriteCategory(id)
@@ -156,7 +157,12 @@ class FavouritesRepository(
val tags = manga.tags.toEntities()
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga.toEntity(), tags)
val entity = FavouriteEntity(manga.id, categoryId, System.currentTimeMillis())
val entity = FavouriteEntity(
mangaId = manga.id,
categoryId = categoryId,
createdAt = System.currentTimeMillis(),
sortKey = 0,
)
db.favouritesDao.insert(entity)
}
}
@@ -184,16 +190,4 @@ class FavouritesRepository(
.map { x -> SortOrder(x.order, SortOrder.NEWEST) }
.distinctUntilChanged()
}
private fun groupByCategory(list: List<FavouriteManga>): Map<FavouriteCategory, List<Manga>> {
val map = HashMap<FavouriteCategory, MutableList<Manga>>()
for (item in list) {
val manga = item.manga.toManga(item.tags.toMangaTags())
for (category in item.categories) {
map.getOrPut(category.toFavouriteCategory()) { ArrayList() }
.add(manga)
}
}
return map
}
}

View File

@@ -46,7 +46,7 @@ abstract class HistoryDao {
abstract fun observeCount(): Flow<Int>
@Query("SELECT percent FROM history WHERE manga_id = :id")
abstract fun findProgress(id: Long): Float?
abstract suspend fun findProgress(id: Long): Float?
@Query("DELETE FROM history")
abstract suspend fun clear()

View File

@@ -2,10 +2,15 @@ package org.koitharu.kotatsu.library
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.library.domain.LibraryRepository
import org.koitharu.kotatsu.library.ui.LibraryViewModel
import org.koitharu.kotatsu.library.ui.config.LibraryCategoriesConfigViewModel
val libraryModule
get() = module {
factory { LibraryRepository(get()) }
viewModel { LibraryViewModel(get(), get(), get(), get(), get()) }
viewModel { LibraryCategoriesConfigViewModel(get()) }
}

View File

@@ -0,0 +1,40 @@
package org.koitharu.kotatsu.library.domain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.favourites.data.FavouriteManga
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder
class LibraryRepository(
private val db: MangaDatabase,
) {
fun observeFavourites(order: SortOrder): Flow<Map<FavouriteCategory, List<Manga>>> {
return db.favouritesDao.observeAll(order)
.map { list -> groupByCategory(list) }
}
private fun groupByCategory(list: List<FavouriteManga>): Map<FavouriteCategory, List<Manga>> {
val map = HashMap<FavouriteCategory, MutableList<Manga>>()
for (item in list) {
val manga = item.manga.toManga(item.tags.toMangaTags())
for (category in item.categories) {
if (!category.isVisibleInLibrary) {
continue
}
map.getOrPut(category.toFavouriteCategory()) { ArrayList() }
.add(manga)
}
}
return map
}
}

View File

@@ -61,7 +61,7 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), LibraryListEvent
)
binding.recyclerView.adapter = adapter
binding.recyclerView.setHasFixedSize(true)
addMenuProvider(LibraryMenuProvider(view.context, viewModel))
addMenuProvider(LibraryMenuProvider(view.context, childFragmentManager, viewModel))
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError)

View File

@@ -5,10 +5,11 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import androidx.fragment.app.FragmentManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.dialog.RememberSelectionDialogListener
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.library.ui.config.LibraryCategoriesConfigSheet
import org.koitharu.kotatsu.utils.ext.startOfDay
import java.util.*
import java.util.concurrent.TimeUnit
@@ -16,6 +17,7 @@ import com.google.android.material.R as materialR
class LibraryMenuProvider(
private val context: Context,
private val fragmentManager: FragmentManager,
private val viewModel: LibraryViewModel,
) : MenuProvider {
@@ -30,7 +32,7 @@ class LibraryMenuProvider(
true
}
R.id.action_categories -> {
context.startActivity(FavouriteCategoriesActivity.newIntent(context))
LibraryCategoriesConfigSheet.show(fragmentManager)
true
}
else -> false

View File

@@ -16,10 +16,10 @@ import org.koitharu.kotatsu.core.os.ShortcutsRepository
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.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.MangaWithHistory
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.library.domain.LibraryRepository
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.model.*
@@ -34,8 +34,8 @@ import java.util.*
private const val HISTORY_MAX_SEGMENTS = 2
class LibraryViewModel(
private val repository: LibraryRepository,
private val historyRepository: HistoryRepository,
private val favouritesRepository: FavouritesRepository,
private val shortcutsRepository: ShortcutsRepository,
private val trackingRepository: TrackingRepository,
private val settings: AppSettings,
@@ -45,7 +45,7 @@ class LibraryViewModel(
val content: LiveData<List<ListModel>> = combine(
historyRepository.observeAllWithHistory(),
favouritesRepository.observeAllGrouped(SortOrder.NEWEST),
repository.observeFavourites(SortOrder.NEWEST),
) { history, favourites ->
mapList(history, favourites)
}.catch { e ->

View File

@@ -0,0 +1,32 @@
package org.koitharu.kotatsu.library.ui.config
import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
class LibraryCategoriesConfigAdapter(
listener: OnListItemClickListener<FavouriteCategory>,
) : AsyncListDifferDelegationAdapter<FavouriteCategory>(DiffCallback()) {
init {
delegatesManager.addDelegate(libraryCategoryAD(listener))
}
class DiffCallback : DiffUtil.ItemCallback<FavouriteCategory>() {
override fun areItemsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
return oldItem.isVisibleInLibrary == newItem.isVisibleInLibrary && oldItem.title == newItem.title
}
override fun getChangePayload(oldItem: FavouriteCategory, newItem: FavouriteCategory): Any? {
return if (oldItem.isVisibleInLibrary == newItem.isVisibleInLibrary) {
super.getChangePayload(oldItem, newItem)
} else Unit
}
}
}

View File

@@ -0,0 +1,56 @@
package org.koitharu.kotatsu.library.ui.config
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.databinding.SheetBaseBinding
import org.koitharu.kotatsu.utils.BottomSheetToolbarController
class LibraryCategoriesConfigSheet : BaseBottomSheet<SheetBaseBinding>(), OnListItemClickListener<FavouriteCategory>,
View.OnClickListener {
private val viewModel by viewModel<LibraryCategoriesConfigViewModel>()
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetBaseBinding {
return SheetBaseBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.setNavigationOnClickListener { dismiss() }
binding.toolbar.setTitle(R.string.favourites_categories)
binding.buttonDone.isVisible = true
binding.buttonDone.setOnClickListener(this)
behavior?.addBottomSheetCallback(BottomSheetToolbarController(binding.toolbar))
if (!resources.getBoolean(R.bool.is_tablet)) {
binding.toolbar.navigationIcon = null
}
val adapter = LibraryCategoriesConfigAdapter(this)
binding.recyclerView.adapter = adapter
viewModel.content.observe(viewLifecycleOwner) { adapter.items = it }
}
override fun onItemClick(item: FavouriteCategory, view: View) {
viewModel.toggleItem(item)
}
override fun onClick(v: View?) {
dismiss()
}
companion object {
private const val TAG = "LibraryCategoriesConfigSheet"
fun show(fm: FragmentManager) = LibraryCategoriesConfigSheet().show(fm, TAG)
}
}

View File

@@ -0,0 +1,27 @@
package org.koitharu.kotatsu.library.ui.config
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.FavouriteCategory
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
class LibraryCategoriesConfigViewModel(
private val favouritesRepository: FavouritesRepository,
) : BaseViewModel() {
val content = favouritesRepository.observeCategories()
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
private var updateJob: Job? = null
fun toggleItem(category: FavouriteCategory) {
val prevJob = updateJob
updateJob = launchJob(Dispatchers.Default) {
prevJob?.join()
favouritesRepository.updateCategory(category.id, !category.isVisibleInLibrary)
}
}
}

View File

@@ -0,0 +1,22 @@
package org.koitharu.kotatsu.library.ui.config
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding
fun libraryCategoryAD(
listener: OnListItemClickListener<FavouriteCategory>,
) = adapterDelegateViewBinding<FavouriteCategory, FavouriteCategory, ItemCategoryCheckableMultipleBinding>(
{ layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) }
) {
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
itemView.setOnClickListener(eventListener)
bind {
binding.root.text = item.title
binding.root.isChecked = item.isVisibleInLibrary
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/checkedTextView"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?android:selectableItemBackground"
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
android:gravity="start|center_vertical"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:textAppearance="?attr/textAppearanceBodyLarge"
tools:checked="true"
tools:text="@tools:sample/lorem[4]" />

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@drawable/sheet_toolbar_background">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:navigationIcon="?actionModeCloseDrawable"
tools:title="@string/app_name">
<Button
android:id="@+id/button_done"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
android:text="@string/done"
android:visibility="gone"
tools:visibility="visible" />
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_checkable_new" />
</LinearLayout>