Categories in shelf preview
parent
acdce5c3f7
commit
1415a940f4
@ -0,0 +1,25 @@
|
||||
package org.xtimms.tokusho.core.database
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.xtimms.tokusho.R
|
||||
|
||||
class DatabasePrePopulateCallback(private val resources: Resources) : RoomDatabase.Callback() {
|
||||
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL(
|
||||
"INSERT INTO favourite_categories (created_at, sort_key, title, `order`, track, show_in_lib, `deleted_at`) VALUES (?,?,?,?,?,?,?)",
|
||||
arrayOf(
|
||||
System.currentTimeMillis(),
|
||||
1,
|
||||
resources.getString(R.string.read_later),
|
||||
SortOrder.NEWEST.name,
|
||||
1,
|
||||
1,
|
||||
0L,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package org.xtimms.tokusho.core.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.xtimms.tokusho.core.database.entity.FavouriteCategoryEntity
|
||||
|
||||
@Dao
|
||||
abstract class FavouriteCategoriesDao {
|
||||
|
||||
@Query("SELECT * FROM favourite_categories WHERE category_id = :id AND deleted_at = 0")
|
||||
abstract suspend fun find(id: Int): FavouriteCategoryEntity
|
||||
|
||||
@Query("SELECT * FROM favourite_categories WHERE deleted_at = 0 ORDER BY sort_key")
|
||||
abstract suspend fun findAll(): List<FavouriteCategoryEntity>
|
||||
|
||||
@Query("SELECT * FROM favourite_categories WHERE deleted_at = 0 ORDER BY sort_key")
|
||||
abstract fun observeAll(): Flow<List<FavouriteCategoryEntity>>
|
||||
|
||||
@Query("SELECT * FROM favourite_categories WHERE deleted_at = 0 AND show_in_lib = 1 ORDER BY sort_key")
|
||||
abstract fun observeAllForLibrary(): Flow<List<FavouriteCategoryEntity>>
|
||||
|
||||
@Query("SELECT * FROM favourite_categories WHERE category_id = :id AND deleted_at = 0")
|
||||
abstract fun observe(id: Long): Flow<FavouriteCategoryEntity?>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
abstract suspend fun insert(category: FavouriteCategoryEntity): Long
|
||||
|
||||
suspend fun delete(id: Long) = setDeletedAt(id, System.currentTimeMillis())
|
||||
|
||||
@Query("UPDATE favourite_categories SET title = :title, `order` = :order, `track` = :tracker, `show_in_lib` = :onShelf WHERE category_id = :id")
|
||||
abstract suspend fun update(id: Long, title: String, order: String, tracker: Boolean, onShelf: Boolean)
|
||||
|
||||
@Query("UPDATE favourite_categories SET `order` = :order WHERE category_id = :id")
|
||||
abstract suspend fun updateOrder(id: Long, order: String)
|
||||
|
||||
@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)
|
||||
|
||||
@Query("DELETE FROM favourite_categories WHERE deleted_at != 0 AND deleted_at < :maxDeletionTime")
|
||||
abstract suspend fun gc(maxDeletionTime: Long)
|
||||
|
||||
@Query("SELECT MAX(sort_key) FROM favourite_categories WHERE deleted_at = 0")
|
||||
protected abstract suspend fun getMaxSortKey(): Int?
|
||||
|
||||
suspend fun getNextSortKey(): Int {
|
||||
return (getMaxSortKey() ?: 0) + 1
|
||||
}
|
||||
|
||||
@Upsert
|
||||
abstract suspend fun upsert(entity: FavouriteCategoryEntity)
|
||||
|
||||
@Query("UPDATE favourite_categories SET deleted_at = :deletedAt WHERE category_id = :id")
|
||||
protected abstract suspend fun setDeletedAt(id: Long, deletedAt: Long)
|
||||
}
|
||||
@ -0,0 +1,169 @@
|
||||
package org.xtimms.tokusho.core.database.dao
|
||||
|
||||
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.room.Upsert
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.intellij.lang.annotations.Language
|
||||
import org.xtimms.tokusho.core.database.entity.FavouriteEntity
|
||||
import org.xtimms.tokusho.core.database.entity.MangaEntity
|
||||
import org.xtimms.tokusho.core.model.ListSortOrder
|
||||
import org.xtimms.tokusho.sections.shelf.FavouriteManga
|
||||
|
||||
@Dao
|
||||
abstract class FavouritesDao {
|
||||
|
||||
/** SELECT **/
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM favourites WHERE deleted_at = 0 GROUP BY manga_id ORDER BY created_at DESC")
|
||||
abstract suspend fun findAll(): List<FavouriteManga>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM favourites WHERE deleted_at = 0 GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit")
|
||||
abstract suspend fun findLast(limit: Int): List<FavouriteManga>
|
||||
|
||||
fun observeAll(order: ListSortOrder): Flow<List<FavouriteManga>> {
|
||||
val orderBy = getOrderBy(order)
|
||||
|
||||
@Language("RoomSql")
|
||||
val query = SimpleSQLiteQuery(
|
||||
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
|
||||
"WHERE favourites.deleted_at = 0 GROUP BY favourites.manga_id ORDER BY $orderBy",
|
||||
)
|
||||
return observeAllImpl(query)
|
||||
}
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT * FROM favourites WHERE deleted_at = 0 " +
|
||||
"GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
|
||||
)
|
||||
abstract suspend fun findAll(offset: Int, limit: Int): List<FavouriteManga>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM favourites WHERE deleted_at = 0 ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
|
||||
abstract suspend fun findAllRaw(offset: Int, limit: Int): List<FavouriteManga>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT * FROM favourites WHERE category_id = :categoryId AND deleted_at = 0 " +
|
||||
"GROUP BY manga_id ORDER BY created_at DESC",
|
||||
)
|
||||
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga>
|
||||
|
||||
fun observeAll(categoryId: Long, order: ListSortOrder): Flow<List<FavouriteManga>> {
|
||||
val orderBy = getOrderBy(order)
|
||||
|
||||
@Language("RoomSql")
|
||||
val query = SimpleSQLiteQuery(
|
||||
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
|
||||
"WHERE category_id = ? AND deleted_at = 0 GROUP BY favourites.manga_id ORDER BY $orderBy",
|
||||
arrayOf<Any>(categoryId),
|
||||
)
|
||||
return observeAllImpl(query)
|
||||
}
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT * FROM favourites WHERE category_id = :categoryId AND deleted_at = 0 " +
|
||||
"GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
|
||||
)
|
||||
abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List<FavouriteManga>
|
||||
|
||||
@Query(
|
||||
"SELECT * FROM manga WHERE manga_id IN " +
|
||||
"(SELECT manga_id FROM favourites WHERE category_id = :categoryId AND deleted_at = 0)",
|
||||
)
|
||||
abstract suspend fun findAllManga(categoryId: Int): List<MangaEntity>
|
||||
|
||||
@Query("SELECT COUNT(DISTINCT manga_id) FROM favourites WHERE deleted_at = 0")
|
||||
abstract fun observeMangaCount(): Flow<Int>
|
||||
|
||||
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites WHERE deleted_at = 0)")
|
||||
abstract suspend fun findAllManga(): List<MangaEntity>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM favourites WHERE manga_id = :id AND deleted_at = 0 GROUP BY manga_id")
|
||||
abstract suspend fun find(id: Long): FavouriteManga?
|
||||
|
||||
@Transaction
|
||||
@Deprecated("Ignores order")
|
||||
@Query("SELECT * FROM favourites WHERE manga_id = :id AND deleted_at = 0 GROUP BY manga_id")
|
||||
abstract fun observe(id: Long): Flow<FavouriteManga?>
|
||||
|
||||
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id = :id AND deleted_at = 0")
|
||||
abstract fun observeIds(id: Long): Flow<List<Long>>
|
||||
|
||||
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id IN (:mangaIds) AND deleted_at = 0")
|
||||
abstract suspend fun findCategoriesIds(mangaIds: Collection<Long>): List<Long>
|
||||
|
||||
/** INSERT **/
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun insert(favourite: FavouriteEntity)
|
||||
|
||||
/** DELETE **/
|
||||
|
||||
suspend fun delete(mangaId: Long) = setDeletedAt(
|
||||
mangaId = mangaId,
|
||||
deletedAt = System.currentTimeMillis(),
|
||||
)
|
||||
|
||||
suspend fun delete(mangaId: Long, categoryId: Long) = setDeletedAt(
|
||||
categoryId = categoryId,
|
||||
mangaId = mangaId,
|
||||
deletedAt = System.currentTimeMillis(),
|
||||
)
|
||||
|
||||
suspend fun deleteAll(categoryId: Long) = setDeletedAtAll(
|
||||
categoryId = categoryId,
|
||||
deletedAt = System.currentTimeMillis(),
|
||||
)
|
||||
|
||||
suspend fun recover(mangaId: Long) = setDeletedAt(
|
||||
mangaId = mangaId,
|
||||
deletedAt = 0L,
|
||||
)
|
||||
|
||||
suspend fun recover(categoryId: Long, mangaId: Long) = setDeletedAt(
|
||||
categoryId = categoryId,
|
||||
mangaId = mangaId,
|
||||
deletedAt = 0L,
|
||||
)
|
||||
|
||||
@Query("DELETE FROM favourites WHERE deleted_at != 0 AND deleted_at < :maxDeletionTime")
|
||||
abstract suspend fun gc(maxDeletionTime: Long)
|
||||
|
||||
/** TOOLS **/
|
||||
|
||||
@Upsert
|
||||
abstract suspend fun upsert(entity: FavouriteEntity)
|
||||
|
||||
@Transaction
|
||||
@RawQuery(observedEntities = [FavouriteEntity::class])
|
||||
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<FavouriteManga>>
|
||||
|
||||
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId")
|
||||
protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long)
|
||||
|
||||
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId AND category_id = :categoryId")
|
||||
abstract suspend fun setDeletedAt(categoryId: Long, mangaId: Long, deletedAt: Long)
|
||||
|
||||
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE category_id = :categoryId AND deleted_at = 0")
|
||||
protected abstract suspend fun setDeletedAtAll(categoryId: Long, deletedAt: Long)
|
||||
|
||||
private fun getOrderBy(sortOrder: ListSortOrder) = when (sortOrder) {
|
||||
ListSortOrder.NEWEST -> "favourites.created_at DESC"
|
||||
ListSortOrder.ALPHABETIC -> "manga.title ASC"
|
||||
ListSortOrder.PROGRESS -> "IFNULL((SELECT percent FROM history WHERE history.manga_id = manga.manga_id), 0) DESC"
|
||||
|
||||
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package org.xtimms.tokusho.core.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import org.xtimms.tokusho.core.database.TABLE_FAVOURITE_CATEGORIES
|
||||
|
||||
@Entity(tableName = TABLE_FAVOURITE_CATEGORIES)
|
||||
data class FavouriteCategoryEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@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 = "order") val order: String,
|
||||
@ColumnInfo(name = "track") val track: Boolean,
|
||||
@ColumnInfo(name = "show_in_lib") val isVisibleInLibrary: Boolean,
|
||||
@ColumnInfo(name = "deleted_at") val deletedAt: Long,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as FavouriteCategoryEntity
|
||||
|
||||
if (categoryId != other.categoryId) return false
|
||||
if (createdAt != other.createdAt) return false
|
||||
if (sortKey != other.sortKey) return false
|
||||
if (title != other.title) return false
|
||||
if (order != other.order) return false
|
||||
if (track != other.track) return false
|
||||
return isVisibleInLibrary == other.isVisibleInLibrary
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = categoryId
|
||||
result = 31 * result + createdAt.hashCode()
|
||||
result = 31 * result + sortKey
|
||||
result = 31 * result + title.hashCode()
|
||||
result = 31 * result + order.hashCode()
|
||||
result = 31 * result + track.hashCode()
|
||||
result = 31 * result + isVisibleInLibrary.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package org.xtimms.tokusho.core.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import org.xtimms.tokusho.core.database.TABLE_FAVOURITES
|
||||
|
||||
@Entity(
|
||||
tableName = TABLE_FAVOURITES,
|
||||
primaryKeys = ["manga_id", "category_id"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
childColumns = ["manga_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = FavouriteCategoryEntity::class,
|
||||
parentColumns = ["category_id"],
|
||||
childColumns = ["category_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
data class FavouriteEntity(
|
||||
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
||||
@ColumnInfo(name = "category_id", index = true) val categoryId: Long,
|
||||
@ColumnInfo(name = "sort_key") val sortKey: Int,
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long,
|
||||
@ColumnInfo(name = "deleted_at") val deletedAt: Long,
|
||||
)
|
||||
@ -0,0 +1,21 @@
|
||||
package org.xtimms.tokusho.core.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.time.Instant
|
||||
|
||||
@Parcelize
|
||||
data class FavouriteCategory(
|
||||
val id: Long,
|
||||
val title: String,
|
||||
val sortKey: Int,
|
||||
val order: ListSortOrder,
|
||||
val createdAt: Instant,
|
||||
val isTrackingEnabled: Boolean,
|
||||
val isVisibleInLibrary: Boolean,
|
||||
) : Parcelable, ListModel {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is FavouriteCategory && id == other.id
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,134 @@
|
||||
package org.xtimms.tokusho.data.repository
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import dagger.Reusable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.xtimms.tokusho.core.database.TokushoDatabase
|
||||
import org.xtimms.tokusho.core.database.entity.toFavouriteCategory
|
||||
import org.xtimms.tokusho.core.database.entity.toManga
|
||||
import org.xtimms.tokusho.core.model.FavouriteCategory
|
||||
import org.xtimms.tokusho.core.model.ListSortOrder
|
||||
import org.xtimms.tokusho.utils.ReversibleHandle
|
||||
import org.xtimms.tokusho.utils.lang.mapItems
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class FavouritesRepository @Inject constructor(
|
||||
private val db: TokushoDatabase,
|
||||
) {
|
||||
|
||||
fun observeAll(categoryId: Long, order: ListSortOrder): Flow<List<Manga>> {
|
||||
return db.getFavouritesDao().observeAll(categoryId, order)
|
||||
.mapItems { it.toManga() }
|
||||
}
|
||||
|
||||
fun observeAll(categoryId: Long): Flow<List<Manga>> {
|
||||
return observeOrder(categoryId)
|
||||
.flatMapLatest { order -> observeAll(categoryId, order) }
|
||||
}
|
||||
|
||||
fun observeMangaCount(): Flow<Int> {
|
||||
return db.getFavouritesDao().observeMangaCount()
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun observeCategoriesForLibrary(): Flow<List<FavouriteCategory>> {
|
||||
return db.getFavouriteCategoriesDao().observeAllForLibrary().mapItems {
|
||||
it.toFavouriteCategory()
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun observeCategoriesIds(mangaId: Long): Flow<Set<Long>> {
|
||||
return db.getFavouritesDao().observeIds(mangaId).map { it.toSet() }
|
||||
}
|
||||
|
||||
suspend fun getCategoriesIds(mangaIds: Collection<Long>): Set<Long> {
|
||||
return db.getFavouritesDao().findCategoriesIds(mangaIds).toSet()
|
||||
}
|
||||
|
||||
suspend fun updateCategory(
|
||||
id: Long,
|
||||
title: String,
|
||||
sortOrder: ListSortOrder,
|
||||
isTrackerEnabled: Boolean,
|
||||
isVisibleOnShelf: Boolean,
|
||||
) {
|
||||
db.getFavouriteCategoriesDao().update(id, title, sortOrder.name, isTrackerEnabled, isVisibleOnShelf)
|
||||
}
|
||||
|
||||
suspend fun updateCategory(id: Long, isVisibleInLibrary: Boolean) {
|
||||
db.getFavouriteCategoriesDao().updateLibVisibility(id, isVisibleInLibrary)
|
||||
}
|
||||
|
||||
suspend fun updateCategoryTracking(id: Long, isTrackingEnabled: Boolean) {
|
||||
db.getFavouriteCategoriesDao().updateTracking(id, isTrackingEnabled)
|
||||
}
|
||||
|
||||
suspend fun removeCategories(ids: Collection<Long>) {
|
||||
db.withTransaction {
|
||||
for (id in ids) {
|
||||
db.getFavouritesDao().deleteAll(id)
|
||||
db.getFavouriteCategoriesDao().delete(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setCategoryOrder(id: Long, order: ListSortOrder) {
|
||||
db.getFavouriteCategoriesDao().updateOrder(id, order.name)
|
||||
}
|
||||
|
||||
suspend fun reorderCategories(orderedIds: List<Long>) {
|
||||
val dao = db.getFavouriteCategoriesDao()
|
||||
db.withTransaction {
|
||||
for ((i, id) in orderedIds.withIndex()) {
|
||||
dao.updateSortKey(id, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeFromFavourites(ids: Collection<Long>): ReversibleHandle {
|
||||
db.withTransaction {
|
||||
for (id in ids) {
|
||||
db.getFavouritesDao().delete(mangaId = id)
|
||||
}
|
||||
}
|
||||
return ReversibleHandle { recoverToFavourites(ids) }
|
||||
}
|
||||
|
||||
suspend fun removeFromCategory(categoryId: Long, ids: Collection<Long>): ReversibleHandle {
|
||||
db.withTransaction {
|
||||
for (id in ids) {
|
||||
db.getFavouritesDao().delete(categoryId = categoryId, mangaId = id)
|
||||
}
|
||||
}
|
||||
return ReversibleHandle { recoverToCategory(categoryId, ids) }
|
||||
}
|
||||
|
||||
private fun observeOrder(categoryId: Long): Flow<ListSortOrder> {
|
||||
return db.getFavouriteCategoriesDao().observe(categoryId)
|
||||
.filterNotNull()
|
||||
.map { x -> ListSortOrder(x.order, ListSortOrder.NEWEST) }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private suspend fun recoverToFavourites(ids: Collection<Long>) {
|
||||
db.withTransaction {
|
||||
for (id in ids) {
|
||||
db.getFavouritesDao().recover(mangaId = id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun recoverToCategory(categoryId: Long, ids: Collection<Long>) {
|
||||
db.withTransaction {
|
||||
for (id in ids) {
|
||||
db.getFavouritesDao().recover(mangaId = id, categoryId = categoryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package org.xtimms.tokusho.sections.settings.shelf
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Category
|
||||
import androidx.compose.material.icons.outlined.Update
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import org.xtimms.tokusho.R
|
||||
import org.xtimms.tokusho.core.components.PreferenceItem
|
||||
import org.xtimms.tokusho.core.components.PreferenceSubtitle
|
||||
import org.xtimms.tokusho.core.components.ScaffoldWithTopAppBar
|
||||
import org.xtimms.tokusho.sections.shelf.ShelfViewModel
|
||||
|
||||
const val SHELF_SETTINGS_DESTINATION = "shelf_settings"
|
||||
|
||||
@Composable
|
||||
fun ShelfSettingsView(
|
||||
shelfViewModel: ShelfViewModel = hiltViewModel(),
|
||||
navigateBack: () -> Unit,
|
||||
navigateToCategories: () -> Unit
|
||||
) {
|
||||
|
||||
ScaffoldWithTopAppBar(
|
||||
title = stringResource(R.string.nav_shelf),
|
||||
navigateBack = navigateBack
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(padding),
|
||||
contentPadding = PaddingValues(
|
||||
bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
)
|
||||
) {
|
||||
item {
|
||||
PreferenceSubtitle(text = stringResource(id = R.string.categories))
|
||||
}
|
||||
item {
|
||||
PreferenceItem(
|
||||
title = stringResource(id = R.string.edit_categories),
|
||||
description = pluralStringResource(
|
||||
id = R.plurals.categories_count,
|
||||
count = shelfViewModel.uiState.value.categories.size,
|
||||
shelfViewModel.uiState.value.categories.size
|
||||
),
|
||||
icon = Icons.Outlined.Category,
|
||||
onClick = {
|
||||
navigateToCategories()
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceSubtitle(text = stringResource(id = R.string.updates))
|
||||
}
|
||||
item {
|
||||
PreferenceItem(
|
||||
title = stringResource(id = R.string.auto_update),
|
||||
description = "Off",
|
||||
icon = Icons.Outlined.Update
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package org.xtimms.tokusho.sections.settings.shelf.categories
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.NewLabel
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.xtimms.tokusho.R
|
||||
import org.xtimms.tokusho.core.components.ScaffoldWithClassicTopAppBar
|
||||
|
||||
const val CATEGORIES_DESTINATION = "categories"
|
||||
|
||||
@Composable
|
||||
fun CategoriesView(
|
||||
navigateBack: () -> Unit,
|
||||
) {
|
||||
|
||||
ScaffoldWithClassicTopAppBar(
|
||||
title = stringResource(R.string.edit_categories),
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = { }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.NewLabel,
|
||||
contentDescription = "New category"
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.add),
|
||||
modifier = Modifier.padding(start = 16.dp, end = 8.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
navigateBack = navigateBack
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(padding),
|
||||
contentPadding = PaddingValues(
|
||||
bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
)
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package org.xtimms.tokusho.sections.shelf
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Junction
|
||||
import androidx.room.Relation
|
||||
import org.xtimms.tokusho.core.database.entity.FavouriteCategoryEntity
|
||||
import org.xtimms.tokusho.core.database.entity.FavouriteEntity
|
||||
import org.xtimms.tokusho.core.database.entity.MangaEntity
|
||||
import org.xtimms.tokusho.core.database.entity.MangaTagsEntity
|
||||
import org.xtimms.tokusho.core.database.entity.TagEntity
|
||||
|
||||
class FavouriteManga(
|
||||
@Embedded val favourite: FavouriteEntity,
|
||||
@Relation(
|
||||
parentColumn = "manga_id",
|
||||
entityColumn = "manga_id"
|
||||
)
|
||||
val manga: MangaEntity,
|
||||
@Relation(
|
||||
parentColumn = "category_id",
|
||||
entityColumn = "category_id"
|
||||
)
|
||||
val categories: List<FavouriteCategoryEntity>,
|
||||
@Relation(
|
||||
parentColumn = "manga_id",
|
||||
entityColumn = "tag_id",
|
||||
associateBy = Junction(MangaTagsEntity::class)
|
||||
)
|
||||
val tags: List<TagEntity>
|
||||
)
|
||||
@ -0,0 +1,14 @@
|
||||
package org.xtimms.tokusho.sections.shelf
|
||||
|
||||
import org.xtimms.tokusho.core.base.state.UiState
|
||||
import org.xtimms.tokusho.core.model.FavouriteCategory
|
||||
|
||||
data class ShelfUiState(
|
||||
val categories: List<FavouriteCategory> = emptyList(),
|
||||
override val isLoading: Boolean = false,
|
||||
override val message: String? = null,
|
||||
) : UiState() {
|
||||
|
||||
override fun setLoading(value: Boolean) = copy(isLoading = value)
|
||||
override fun setMessage(value: String?) = copy(message = value)
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package org.xtimms.tokusho.sections.shelf
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.plus
|
||||
import org.xtimms.tokusho.core.base.viewmodel.BaseViewModel
|
||||
import org.xtimms.tokusho.data.repository.FavouritesRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ShelfViewModel @Inject constructor(
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
) : BaseViewModel<ShelfUiState>() {
|
||||
|
||||
private val categoriesStateFlow = favouritesRepository.observeCategoriesForLibrary()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||
|
||||
val isEmpty = categoriesStateFlow.map {
|
||||
it?.isEmpty() == true
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||
|
||||
init {
|
||||
launchJob(Dispatchers.Default) {
|
||||
mutableUiState.update {
|
||||
it.copy(
|
||||
categories = categoriesStateFlow.value ?: emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val mutableUiState = MutableStateFlow(ShelfUiState())
|
||||
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package org.xtimms.tokusho.utils
|
||||
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.xtimms.tokusho.utils.lang.processLifecycleScope
|
||||
|
||||
fun interface ReversibleHandle {
|
||||
|
||||
suspend fun reverse()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default, CoroutineStart.ATOMIC) {
|
||||
runCatchingCancellable {
|
||||
withContext(NonCancellable) {
|
||||
reverse()
|
||||
}
|
||||
}.onFailure {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
operator fun ReversibleHandle.plus(other: ReversibleHandle) = ReversibleHandle {
|
||||
this.reverse()
|
||||
other.reverse()
|
||||
}
|
||||
Loading…
Reference in New Issue