Categories in shelf preview
This commit is contained in:
@@ -17,7 +17,7 @@ import org.acra.data.StringFormat
|
||||
import org.acra.ktx.initAcra
|
||||
import org.acra.sender.HttpSender
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.xtimms.tokusho.core.database.MangaDatabase
|
||||
import org.xtimms.tokusho.core.database.TokushoDatabase
|
||||
import org.xtimms.tokusho.core.prefs.AppSettings
|
||||
import org.xtimms.tokusho.core.updates.Updater
|
||||
import org.xtimms.tokusho.utils.lang.processLifecycleScope
|
||||
@@ -28,7 +28,7 @@ import javax.inject.Provider
|
||||
class App : Application() {
|
||||
|
||||
@Inject
|
||||
lateinit var database: Provider<MangaDatabase>
|
||||
lateinit var database: Provider<TokushoDatabase>
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
@@ -20,7 +20,7 @@ import org.xtimms.tokusho.core.cache.CacheDir
|
||||
import org.xtimms.tokusho.core.cache.ContentCache
|
||||
import org.xtimms.tokusho.core.cache.MemoryContentCache
|
||||
import org.xtimms.tokusho.core.cache.StubContentCache
|
||||
import org.xtimms.tokusho.core.database.MangaDatabase
|
||||
import org.xtimms.tokusho.core.database.TokushoDatabase
|
||||
import org.xtimms.tokusho.core.network.MangaHttpClient
|
||||
import org.xtimms.tokusho.core.os.NetworkState
|
||||
import org.xtimms.tokusho.core.parser.MangaLoaderContextImpl
|
||||
@@ -53,8 +53,8 @@ interface TokushoModule {
|
||||
@Singleton
|
||||
fun provideMangaDatabase(
|
||||
@ApplicationContext context: Context,
|
||||
): MangaDatabase {
|
||||
return MangaDatabase(context)
|
||||
): TokushoDatabase {
|
||||
return TokushoDatabase(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -2,8 +2,6 @@ package org.xtimms.tokusho.core
|
||||
|
||||
import android.graphics.Path
|
||||
import android.view.animation.PathInterpolator
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationVector1D
|
||||
import androidx.compose.animation.core.Easing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -19,7 +17,6 @@ import androidx.navigation.navArgument
|
||||
import coil.ImageLoader
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.xtimms.tokusho.core.logs.FileLogger
|
||||
import org.xtimms.tokusho.core.model.ShelfCategory
|
||||
import org.xtimms.tokusho.core.motion.materialSharedAxisXIn
|
||||
import org.xtimms.tokusho.core.motion.materialSharedAxisXOut
|
||||
import org.xtimms.tokusho.sections.details.DETAILS_DESTINATION
|
||||
@@ -46,6 +43,10 @@ import org.xtimms.tokusho.sections.settings.appearance.DARK_THEME_DESTINATION
|
||||
import org.xtimms.tokusho.sections.settings.appearance.DarkThemeView
|
||||
import org.xtimms.tokusho.sections.settings.appearance.LANGUAGES_DESTINATION
|
||||
import org.xtimms.tokusho.sections.settings.appearance.LanguagesView
|
||||
import org.xtimms.tokusho.sections.settings.shelf.SHELF_SETTINGS_DESTINATION
|
||||
import org.xtimms.tokusho.sections.settings.shelf.ShelfSettingsView
|
||||
import org.xtimms.tokusho.sections.settings.shelf.categories.CATEGORIES_DESTINATION
|
||||
import org.xtimms.tokusho.sections.settings.shelf.categories.CategoriesView
|
||||
import org.xtimms.tokusho.sections.settings.storage.STORAGE_DESTINATION
|
||||
import org.xtimms.tokusho.sections.settings.storage.StorageView
|
||||
import org.xtimms.tokusho.sections.shelf.ShelfMap
|
||||
@@ -103,10 +104,6 @@ fun Navigation(
|
||||
composable(BottomNavDestination.Shelf.route) {
|
||||
val library: ShelfMap = emptyMap()
|
||||
ShelfView(
|
||||
categories = listOf(
|
||||
ShelfCategory(1, "Test 1", 1L, 1L),
|
||||
ShelfCategory(2, "Test 2", 2L, 2L)
|
||||
),
|
||||
currentPage = { 0 },
|
||||
showPageTabs = true,
|
||||
getNumberOfMangaForCategory = { 2 },
|
||||
@@ -151,6 +148,7 @@ fun Navigation(
|
||||
navigateToAppearance = { navController.navigate(APPEARANCE_DESTINATION) },
|
||||
navigateToAbout = { navController.navigate(ABOUT_DESTINATION) },
|
||||
navigateToAdvanced = { navController.navigate(ADVANCED_DESTINATION) },
|
||||
navigateToShelfSettings = { navController.navigate(SHELF_SETTINGS_DESTINATION) },
|
||||
navigateToStorage = { navController.navigate(STORAGE_DESTINATION) }
|
||||
)
|
||||
}
|
||||
@@ -176,6 +174,19 @@ fun Navigation(
|
||||
)
|
||||
}
|
||||
|
||||
composable(SHELF_SETTINGS_DESTINATION) {
|
||||
ShelfSettingsView(
|
||||
navigateBack = navigateBack,
|
||||
navigateToCategories = { navController.navigate(CATEGORIES_DESTINATION) }
|
||||
)
|
||||
}
|
||||
|
||||
composable(CATEGORIES_DESTINATION) {
|
||||
CategoriesView(
|
||||
navigateBack = navigateBack,
|
||||
)
|
||||
}
|
||||
|
||||
composable(STORAGE_DESTINATION) {
|
||||
StorageView(
|
||||
navigateBack = navigateBack,
|
||||
|
||||
@@ -4,10 +4,12 @@ import android.net.Uri
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -15,11 +17,15 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.net.toUri
|
||||
import coil.ImageLoader
|
||||
import org.xtimms.tokusho.core.AsyncImageImpl
|
||||
import org.xtimms.tokusho.ui.theme.TokushoTheme
|
||||
|
||||
@Composable
|
||||
fun SourceItem(
|
||||
@@ -37,6 +43,12 @@ fun SourceItem(
|
||||
.clickable(onClick = onClick)
|
||||
.padding(start = 8.dp, end = 8.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.size(96.dp)
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.aspectRatio(1f)
|
||||
) {
|
||||
AsyncImageImpl(
|
||||
coil = coil,
|
||||
@@ -44,10 +56,10 @@ fun SourceItem(
|
||||
contentDescription = "favicon",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = modifier
|
||||
.size(96.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.aspectRatio(1f)
|
||||
.fillMaxSize()
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
@@ -61,3 +73,16 @@ fun SourceItem(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SourceItemPreview() {
|
||||
TokushoTheme {
|
||||
SourceItem(
|
||||
coil = ImageLoader(LocalContext.current),
|
||||
faviconUrl = "".toUri(),
|
||||
title = "Test",
|
||||
onClick = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,3 +5,5 @@ const val TABLE_TAGS = "tags"
|
||||
const val TABLE_MANGA_TAGS = "manga_tags"
|
||||
const val TABLE_SOURCES = "sources"
|
||||
const val TABLE_HISTORY = "history"
|
||||
const val TABLE_FAVOURITES = "favourites"
|
||||
const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories"
|
||||
@@ -10,9 +10,13 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.xtimms.tokusho.core.database.dao.FavouriteCategoriesDao
|
||||
import org.xtimms.tokusho.core.database.dao.FavouritesDao
|
||||
import org.xtimms.tokusho.core.database.dao.HistoryDao
|
||||
import org.xtimms.tokusho.core.database.dao.MangaDao
|
||||
import org.xtimms.tokusho.core.database.dao.MangaSourcesDao
|
||||
import org.xtimms.tokusho.core.database.entity.FavouriteCategoryEntity
|
||||
import org.xtimms.tokusho.core.database.entity.FavouriteEntity
|
||||
import org.xtimms.tokusho.core.database.entity.HistoryEntity
|
||||
import org.xtimms.tokusho.core.database.entity.MangaEntity
|
||||
import org.xtimms.tokusho.core.database.entity.MangaSourceEntity
|
||||
@@ -28,11 +32,13 @@ const val DATABASE_VERSION = 1
|
||||
TagEntity::class,
|
||||
MangaTagsEntity::class,
|
||||
MangaSourceEntity::class,
|
||||
HistoryEntity::class
|
||||
HistoryEntity::class,
|
||||
FavouriteEntity::class,
|
||||
FavouriteCategoryEntity::class
|
||||
],
|
||||
version = DATABASE_VERSION
|
||||
)
|
||||
abstract class MangaDatabase : RoomDatabase() {
|
||||
abstract class TokushoDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun getHistoryDao(): HistoryDao
|
||||
|
||||
@@ -40,10 +46,15 @@ abstract class MangaDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun getSourcesDao(): MangaSourcesDao
|
||||
|
||||
abstract fun getFavouritesDao(): FavouritesDao
|
||||
|
||||
abstract fun getFavouriteCategoriesDao(): FavouriteCategoriesDao
|
||||
|
||||
}
|
||||
|
||||
fun MangaDatabase(context: Context): MangaDatabase = Room
|
||||
.databaseBuilder(context, MangaDatabase::class.java, "tokusho-db")
|
||||
fun TokushoDatabase(context: Context): TokushoDatabase = Room
|
||||
.databaseBuilder(context, TokushoDatabase::class.java, "tokusho-db.db")
|
||||
.addCallback(DatabasePrePopulateCallback(context.resources))
|
||||
.build()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,11 @@ import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import org.xtimms.tokusho.core.model.FavouriteCategory
|
||||
import org.xtimms.tokusho.core.model.ListSortOrder
|
||||
import org.xtimms.tokusho.core.model.MangaHistory
|
||||
import org.xtimms.tokusho.core.model.MangaSource
|
||||
import org.xtimms.tokusho.sections.shelf.FavouriteManga
|
||||
import org.xtimms.tokusho.utils.lang.longHashCode
|
||||
import java.time.Instant
|
||||
|
||||
@@ -41,6 +44,20 @@ fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
|
||||
|
||||
fun MangaWithTags.toManga() = manga.toManga(tags.toMangaTags())
|
||||
|
||||
fun FavouriteCategoryEntity.toFavouriteCategory(id: Long = categoryId.toLong()) = FavouriteCategory(
|
||||
id = id,
|
||||
title = title,
|
||||
sortKey = sortKey,
|
||||
order = ListSortOrder(order, ListSortOrder.NEWEST),
|
||||
createdAt = Instant.ofEpochMilli(createdAt),
|
||||
isTrackingEnabled = track,
|
||||
isVisibleInLibrary = isVisibleInLibrary,
|
||||
)
|
||||
|
||||
fun FavouriteManga.toManga() = manga.toManga(tags.toMangaTags())
|
||||
|
||||
fun Collection<FavouriteManga>.toMangaList() = map { it.toManga() }
|
||||
|
||||
// Model to entity
|
||||
|
||||
fun Manga.toEntity() = MangaEntity(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,14 @@ package org.xtimms.tokusho.core.parser
|
||||
|
||||
import dagger.Reusable
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.xtimms.tokusho.core.database.MangaDatabase
|
||||
import org.xtimms.tokusho.core.database.TokushoDatabase
|
||||
import org.xtimms.tokusho.core.database.entity.toManga
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
@Reusable
|
||||
class MangaDataRepository @Inject constructor(
|
||||
private val db: MangaDatabase,
|
||||
private val db: TokushoDatabase,
|
||||
private val resolverProvider: Provider<MangaLinkResolver>,
|
||||
) {
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import dagger.Reusable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.xtimms.tokusho.core.database.MangaDatabase
|
||||
import org.xtimms.tokusho.core.database.TokushoDatabase
|
||||
import org.xtimms.tokusho.core.database.entity.HistoryEntity
|
||||
import org.xtimms.tokusho.core.database.entity.toMangaHistory
|
||||
import org.xtimms.tokusho.core.model.MangaHistory
|
||||
@@ -15,7 +15,7 @@ const val PROGRESS_NONE = -1f
|
||||
|
||||
@Reusable
|
||||
class HistoryRepository @Inject constructor(
|
||||
private val db: MangaDatabase,
|
||||
private val db: TokushoDatabase,
|
||||
) {
|
||||
|
||||
suspend fun getOne(manga: Manga): MangaHistory? {
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.xtimms.tokusho.data.repository
|
||||
import dagger.Reusable
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.xtimms.tokusho.BuildConfig
|
||||
import org.xtimms.tokusho.core.database.MangaDatabase
|
||||
import org.xtimms.tokusho.core.database.TokushoDatabase
|
||||
import org.xtimms.tokusho.core.database.dao.MangaSourcesDao
|
||||
import java.util.Collections
|
||||
import java.util.EnumSet
|
||||
@@ -11,7 +11,7 @@ import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class MangaSourcesRepository @Inject constructor(
|
||||
private val db: MangaDatabase,
|
||||
private val db: TokushoDatabase,
|
||||
) {
|
||||
|
||||
private val dao: MangaSourcesDao
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.BatterySaver
|
||||
import androidx.compose.material.icons.outlined.Code
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.LocalLibrary
|
||||
import androidx.compose.material.icons.outlined.Palette
|
||||
import androidx.compose.material.icons.outlined.Storage
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -54,6 +55,7 @@ fun SettingsView(
|
||||
navigateToAppearance: () -> Unit,
|
||||
navigateToAbout: () -> Unit,
|
||||
navigateToAdvanced: () -> Unit,
|
||||
navigateToShelfSettings: () -> Unit,
|
||||
navigateToStorage: () -> Unit
|
||||
) {
|
||||
|
||||
@@ -67,6 +69,7 @@ fun SettingsView(
|
||||
navigateToAppearance = navigateToAppearance,
|
||||
navigateToAbout = navigateToAbout,
|
||||
navigateToAdvanced = navigateToAdvanced,
|
||||
navigateToShelfSettings = navigateToShelfSettings,
|
||||
navigateToStorage = navigateToStorage
|
||||
)
|
||||
}
|
||||
@@ -80,6 +83,7 @@ private fun SettingsViewContent(
|
||||
navigateToAppearance: () -> Unit,
|
||||
navigateToAbout: () -> Unit,
|
||||
navigateToAdvanced: () -> Unit,
|
||||
navigateToShelfSettings: () -> Unit,
|
||||
navigateToStorage: () -> Unit
|
||||
) {
|
||||
|
||||
@@ -151,6 +155,14 @@ private fun SettingsViewContent(
|
||||
onClick = navigateToAppearance
|
||||
)
|
||||
}
|
||||
item {
|
||||
SettingItem(
|
||||
title = stringResource(id = R.string.nav_shelf),
|
||||
description = stringResource(id = R.string.shelf_page),
|
||||
icon = Icons.Outlined.LocalLibrary,
|
||||
onClick = navigateToShelfSettings
|
||||
)
|
||||
}
|
||||
item {
|
||||
val allCaches = uiState.httpCacheSize +
|
||||
uiState.pagesCache +
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
@@ -13,15 +13,14 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import org.xtimms.tokusho.core.components.TabText
|
||||
import org.xtimms.tokusho.core.model.ShelfCategory
|
||||
import org.xtimms.tokusho.sections.shelf.ext.visualName
|
||||
import org.xtimms.tokusho.core.model.FavouriteCategory
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun ShelfTabs(
|
||||
categories: List<ShelfCategory>,
|
||||
categories: List<FavouriteCategory>,
|
||||
pagerState: PagerState,
|
||||
getNumberOfMangaForCategory: (ShelfCategory) -> Int?,
|
||||
getNumberOfMangaForCategory: (FavouriteCategory) -> Int?,
|
||||
onTabItemClick: (Int) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
@@ -40,7 +39,7 @@ internal fun ShelfTabs(
|
||||
onClick = { onTabItemClick(index) },
|
||||
text = {
|
||||
TabText(
|
||||
text = category.visualName,
|
||||
text = category.title,
|
||||
badgeCount = getNumberOfMangaForCategory(category),
|
||||
)
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -8,32 +8,35 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.launch
|
||||
import org.xtimms.tokusho.core.collapsable
|
||||
import org.xtimms.tokusho.core.model.FavouriteCategory
|
||||
import org.xtimms.tokusho.core.model.ShelfCategory
|
||||
import org.xtimms.tokusho.ui.theme.TokushoTheme
|
||||
|
||||
const val SHELF_DESTINATION = "shelf"
|
||||
|
||||
@Composable
|
||||
fun ShelfView(
|
||||
categories: List<ShelfCategory>,
|
||||
currentPage: () -> Int,
|
||||
showPageTabs: Boolean,
|
||||
getNumberOfMangaForCategory: (ShelfCategory) -> Int?,
|
||||
getNumberOfMangaForCategory: (FavouriteCategory) -> Int?,
|
||||
getLibraryForPage: (Int) -> List<ShelfItem>,
|
||||
topBarHeightPx: Float,
|
||||
padding: PaddingValues,
|
||||
) {
|
||||
val viewModel: ShelfViewModel = hiltViewModel()
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
ShelfViewContent(
|
||||
categories = categories,
|
||||
uiState = uiState,
|
||||
currentPage = currentPage,
|
||||
showPageTabs = showPageTabs,
|
||||
getNumberOfMangaForCategory = getNumberOfMangaForCategory,
|
||||
@@ -46,10 +49,10 @@ fun ShelfView(
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ShelfViewContent(
|
||||
categories: List<ShelfCategory>,
|
||||
uiState: ShelfUiState,
|
||||
currentPage: () -> Int,
|
||||
showPageTabs: Boolean,
|
||||
getNumberOfMangaForCategory: (ShelfCategory) -> Int?,
|
||||
getNumberOfMangaForCategory: (FavouriteCategory) -> Int?,
|
||||
getLibraryForPage: (Int) -> List<ShelfItem>,
|
||||
topBarHeightPx: Float,
|
||||
topBarOffsetY: Animatable<Float, AnimationVector1D> = Animatable(0f),
|
||||
@@ -66,17 +69,17 @@ fun ShelfViewContent(
|
||||
)
|
||||
.padding(padding)
|
||||
) {
|
||||
val coercedCurrentPage = remember { currentPage().coerceAtMost(categories.lastIndex) }
|
||||
val pagerState = rememberPagerState(coercedCurrentPage) { categories.size }
|
||||
val coercedCurrentPage = remember { currentPage().coerceAtMost(uiState.categories.lastIndex) }
|
||||
val pagerState = rememberPagerState(coercedCurrentPage) { uiState.categories.size }
|
||||
val scope = rememberCoroutineScope()
|
||||
if (showPageTabs && categories.size > 1) {
|
||||
LaunchedEffect(categories) {
|
||||
if (categories.size <= pagerState.currentPage) {
|
||||
pagerState.scrollToPage(categories.size - 1)
|
||||
if (showPageTabs && uiState.categories.size > 1) {
|
||||
LaunchedEffect(uiState.categories) {
|
||||
if (uiState.categories.size <= pagerState.currentPage) {
|
||||
pagerState.scrollToPage(uiState.categories.size - 1)
|
||||
}
|
||||
}
|
||||
ShelfTabs(
|
||||
categories = categories,
|
||||
categories = uiState.categories,
|
||||
pagerState = pagerState,
|
||||
getNumberOfMangaForCategory = getNumberOfMangaForCategory,
|
||||
) { scope.launch { pagerState.animateScrollToPage(it) } }
|
||||
@@ -93,26 +96,4 @@ fun ShelfViewContent(
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ShelfPreview() {
|
||||
val library: ShelfMap = emptyMap()
|
||||
TokushoTheme {
|
||||
Surface {
|
||||
ShelfViewContent(
|
||||
categories = listOf(
|
||||
ShelfCategory(1, "Test 1", 1L, 1L),
|
||||
ShelfCategory(2, "Test 2", 2L, 2L)
|
||||
),
|
||||
currentPage = { 0 },
|
||||
showPageTabs = true,
|
||||
getNumberOfMangaForCategory = { 2 },
|
||||
getLibraryForPage = { library.values.toTypedArray().getOrNull(0).orEmpty() },
|
||||
padding = PaddingValues(),
|
||||
topBarHeightPx = 0f,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias ShelfMap = Map<ShelfCategory, List<ShelfItem>>
|
||||
@@ -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()
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.xtimms.tokusho.utils.lang
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
@@ -14,3 +15,7 @@ fun <T> Flow<T>.onEachWhile(action: suspend (T) -> Boolean): Flow<T> {
|
||||
isCalled = false
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, R> Flow<List<T>>.mapItems(crossinline transform: (T) -> R): Flow<List<R>> {
|
||||
return map { list -> list.map(transform) }
|
||||
}
|
||||
@@ -8,4 +8,8 @@
|
||||
<item quantity="one">%1$d minute</item>
|
||||
<item quantity="other">%1$d minutes</item>
|
||||
</plurals>
|
||||
<plurals name="categories_count">
|
||||
<item quantity="one">%1$d category</item>
|
||||
<item quantity="other">%1$d categories</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
@@ -118,4 +118,10 @@
|
||||
<string name="total_chapters_read">Total chapters read</string>
|
||||
<string name="min_chapters_read">Minimum chapters read</string>
|
||||
<string name="max_chapters_read">Maximum chapters read</string>
|
||||
<string name="read_later">Read later</string>
|
||||
<string name="shelf_page">Categories, new chapters updates</string>
|
||||
<string name="categories">Categories</string>
|
||||
<string name="edit_categories">Edit categories</string>
|
||||
<string name="updates">Updates</string>
|
||||
<string name="add">Add</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user