diff --git a/app/src/main/java/org/xtimms/tokusho/App.kt b/app/src/main/java/org/xtimms/tokusho/App.kt index 2bf027e..5fb30ca 100644 --- a/app/src/main/java/org/xtimms/tokusho/App.kt +++ b/app/src/main/java/org/xtimms/tokusho/App.kt @@ -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 + lateinit var database: Provider override fun onCreate() { super.onCreate() diff --git a/app/src/main/java/org/xtimms/tokusho/TokushoModule.kt b/app/src/main/java/org/xtimms/tokusho/TokushoModule.kt index fcddc2c..9652aff 100644 --- a/app/src/main/java/org/xtimms/tokusho/TokushoModule.kt +++ b/app/src/main/java/org/xtimms/tokusho/TokushoModule.kt @@ -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 diff --git a/app/src/main/java/org/xtimms/tokusho/core/Navigation.kt b/app/src/main/java/org/xtimms/tokusho/core/Navigation.kt index 66914db..af32d2c 100644 --- a/app/src/main/java/org/xtimms/tokusho/core/Navigation.kt +++ b/app/src/main/java/org/xtimms/tokusho/core/Navigation.kt @@ -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, diff --git a/app/src/main/java/org/xtimms/tokusho/core/components/SourceItem.kt b/app/src/main/java/org/xtimms/tokusho/core/components/SourceItem.kt index ef93d49..a894bbf 100644 --- a/app/src/main/java/org/xtimms/tokusho/core/components/SourceItem.kt +++ b/app/src/main/java/org/xtimms/tokusho/core/components/SourceItem.kt @@ -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( @@ -38,16 +44,22 @@ fun SourceItem( .padding(start = 8.dp, end = 8.dp), horizontalAlignment = Alignment.Start ) { - AsyncImageImpl( - coil = coil, - model = faviconUrl, - contentDescription = "favicon", - contentScale = ContentScale.Crop, + Card( modifier = modifier .size(96.dp) - .clip(RoundedCornerShape(8.dp)) + .clip(MaterialTheme.shapes.large) .aspectRatio(1f) - ) + ) { + AsyncImageImpl( + coil = coil, + model = faviconUrl, + contentDescription = "favicon", + contentScale = ContentScale.Crop, + modifier = modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.large) + ) + } Text( text = title, modifier = Modifier @@ -60,4 +72,17 @@ fun SourceItem( maxLines = maxLines ) } +} + +@Preview +@Composable +fun SourceItemPreview() { + TokushoTheme { + SourceItem( + coil = ImageLoader(LocalContext.current), + faviconUrl = "".toUri(), + title = "Test", + onClick = { } + ) + } } \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/core/database/DatabasePrePopulateCallback.kt b/app/src/main/java/org/xtimms/tokusho/core/database/DatabasePrePopulateCallback.kt new file mode 100644 index 0000000..290d964 --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/core/database/DatabasePrePopulateCallback.kt @@ -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, + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/core/database/Tables.kt b/app/src/main/java/org/xtimms/tokusho/core/database/Tables.kt index 5998141..c4502f9 100644 --- a/app/src/main/java/org/xtimms/tokusho/core/database/Tables.kt +++ b/app/src/main/java/org/xtimms/tokusho/core/database/Tables.kt @@ -4,4 +4,6 @@ const val TABLE_MANGA = "manga" const val TABLE_TAGS = "tags" const val TABLE_MANGA_TAGS = "manga_tags" const val TABLE_SOURCES = "sources" -const val TABLE_HISTORY = "history" \ No newline at end of file +const val TABLE_HISTORY = "history" +const val TABLE_FAVOURITES = "favourites" +const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories" \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/core/database/TokushoDatabase.kt b/app/src/main/java/org/xtimms/tokusho/core/database/TokushoDatabase.kt index 1f93f47..ec0b656 100644 --- a/app/src/main/java/org/xtimms/tokusho/core/database/TokushoDatabase.kt +++ b/app/src/main/java/org/xtimms/tokusho/core/database/TokushoDatabase.kt @@ -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) diff --git a/app/src/main/java/org/xtimms/tokusho/core/database/dao/FavouriteCategoriesDao.kt b/app/src/main/java/org/xtimms/tokusho/core/database/dao/FavouriteCategoriesDao.kt new file mode 100644 index 0000000..db49911 --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/core/database/dao/FavouriteCategoriesDao.kt @@ -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 + + @Query("SELECT * FROM favourite_categories WHERE deleted_at = 0 ORDER BY sort_key") + abstract fun observeAll(): Flow> + + @Query("SELECT * FROM favourite_categories WHERE deleted_at = 0 AND show_in_lib = 1 ORDER BY sort_key") + abstract fun observeAllForLibrary(): Flow> + + @Query("SELECT * FROM favourite_categories WHERE category_id = :id AND deleted_at = 0") + abstract fun observe(id: Long): Flow + + @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) +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/core/database/dao/FavouritesDao.kt b/app/src/main/java/org/xtimms/tokusho/core/database/dao/FavouritesDao.kt new file mode 100644 index 0000000..04ffd75 --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/core/database/dao/FavouritesDao.kt @@ -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 + + @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 + + fun observeAll(order: ListSortOrder): Flow> { + 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 + + @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 + + @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 + + fun observeAll(categoryId: Long, order: ListSortOrder): Flow> { + 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(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 + + @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 + + @Query("SELECT COUNT(DISTINCT manga_id) FROM favourites WHERE deleted_at = 0") + abstract fun observeMangaCount(): Flow + + @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites WHERE deleted_at = 0)") + abstract suspend fun findAllManga(): List + + @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 + + @Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id = :id AND deleted_at = 0") + abstract fun observeIds(id: Long): Flow> + + @Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id IN (:mangaIds) AND deleted_at = 0") + abstract suspend fun findCategoriesIds(mangaIds: Collection): List + + /** 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> + + @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") + } +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/core/database/entity/EntityMapping.kt b/app/src/main/java/org/xtimms/tokusho/core/database/entity/EntityMapping.kt index 26f79f8..b9e72a3 100644 --- a/app/src/main/java/org/xtimms/tokusho/core/database/entity/EntityMapping.kt +++ b/app/src/main/java/org/xtimms/tokusho/core/database/entity/EntityMapping.kt @@ -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) = 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.toMangaList() = map { it.toManga() } + // Model to entity fun Manga.toEntity() = MangaEntity( diff --git a/app/src/main/java/org/xtimms/tokusho/core/database/entity/FavouriteCategoryEntity.kt b/app/src/main/java/org/xtimms/tokusho/core/database/entity/FavouriteCategoryEntity.kt new file mode 100644 index 0000000..38f0e5b --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/core/database/entity/FavouriteCategoryEntity.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/core/database/entity/FavouriteEntity.kt b/app/src/main/java/org/xtimms/tokusho/core/database/entity/FavouriteEntity.kt new file mode 100644 index 0000000..8015894 --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/core/database/entity/FavouriteEntity.kt @@ -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, +) \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/core/model/FavouriteCategory.kt b/app/src/main/java/org/xtimms/tokusho/core/model/FavouriteCategory.kt new file mode 100644 index 0000000..254bf85 --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/core/model/FavouriteCategory.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/core/parser/MangaDataRepository.kt b/app/src/main/java/org/xtimms/tokusho/core/parser/MangaDataRepository.kt index 244906c..9681079 100644 --- a/app/src/main/java/org/xtimms/tokusho/core/parser/MangaDataRepository.kt +++ b/app/src/main/java/org/xtimms/tokusho/core/parser/MangaDataRepository.kt @@ -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, ) { diff --git a/app/src/main/java/org/xtimms/tokusho/data/repository/FavouritesRepository.kt b/app/src/main/java/org/xtimms/tokusho/data/repository/FavouritesRepository.kt new file mode 100644 index 0000000..8c801e0 --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/data/repository/FavouritesRepository.kt @@ -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> { + return db.getFavouritesDao().observeAll(categoryId, order) + .mapItems { it.toManga() } + } + + fun observeAll(categoryId: Long): Flow> { + return observeOrder(categoryId) + .flatMapLatest { order -> observeAll(categoryId, order) } + } + + fun observeMangaCount(): Flow { + return db.getFavouritesDao().observeMangaCount() + .distinctUntilChanged() + } + + fun observeCategoriesForLibrary(): Flow> { + return db.getFavouriteCategoriesDao().observeAllForLibrary().mapItems { + it.toFavouriteCategory() + }.distinctUntilChanged() + } + + fun observeCategoriesIds(mangaId: Long): Flow> { + return db.getFavouritesDao().observeIds(mangaId).map { it.toSet() } + } + + suspend fun getCategoriesIds(mangaIds: Collection): Set { + 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) { + 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) { + val dao = db.getFavouriteCategoriesDao() + db.withTransaction { + for ((i, id) in orderedIds.withIndex()) { + dao.updateSortKey(id, i) + } + } + } + + suspend fun removeFromFavourites(ids: Collection): ReversibleHandle { + db.withTransaction { + for (id in ids) { + db.getFavouritesDao().delete(mangaId = id) + } + } + return ReversibleHandle { recoverToFavourites(ids) } + } + + suspend fun removeFromCategory(categoryId: Long, ids: Collection): 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 { + return db.getFavouriteCategoriesDao().observe(categoryId) + .filterNotNull() + .map { x -> ListSortOrder(x.order, ListSortOrder.NEWEST) } + .distinctUntilChanged() + } + + private suspend fun recoverToFavourites(ids: Collection) { + db.withTransaction { + for (id in ids) { + db.getFavouritesDao().recover(mangaId = id) + } + } + } + + private suspend fun recoverToCategory(categoryId: Long, ids: Collection) { + db.withTransaction { + for (id in ids) { + db.getFavouritesDao().recover(mangaId = id, categoryId = categoryId) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/data/repository/HistoryRepository.kt b/app/src/main/java/org/xtimms/tokusho/data/repository/HistoryRepository.kt index 59ce425..b5339ea 100644 --- a/app/src/main/java/org/xtimms/tokusho/data/repository/HistoryRepository.kt +++ b/app/src/main/java/org/xtimms/tokusho/data/repository/HistoryRepository.kt @@ -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? { diff --git a/app/src/main/java/org/xtimms/tokusho/data/repository/MangaSourcesRepository.kt b/app/src/main/java/org/xtimms/tokusho/data/repository/MangaSourcesRepository.kt index 089356e..fc3b94a 100644 --- a/app/src/main/java/org/xtimms/tokusho/data/repository/MangaSourcesRepository.kt +++ b/app/src/main/java/org/xtimms/tokusho/data/repository/MangaSourcesRepository.kt @@ -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 diff --git a/app/src/main/java/org/xtimms/tokusho/sections/settings/SettingsView.kt b/app/src/main/java/org/xtimms/tokusho/sections/settings/SettingsView.kt index f3cd6df..b045bb4 100644 --- a/app/src/main/java/org/xtimms/tokusho/sections/settings/SettingsView.kt +++ b/app/src/main/java/org/xtimms/tokusho/sections/settings/SettingsView.kt @@ -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 + diff --git a/app/src/main/java/org/xtimms/tokusho/sections/settings/shelf/ShelfSettingsView.kt b/app/src/main/java/org/xtimms/tokusho/sections/settings/shelf/ShelfSettingsView.kt new file mode 100644 index 0000000..e866e31 --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/sections/settings/shelf/ShelfSettingsView.kt @@ -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 + ) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/sections/settings/shelf/categories/CategoriesView.kt b/app/src/main/java/org/xtimms/tokusho/sections/settings/shelf/categories/CategoriesView.kt new file mode 100644 index 0000000..4194e2e --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/sections/settings/shelf/categories/CategoriesView.kt @@ -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() + ) + ) { + + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/sections/shelf/FavouriteManga.kt b/app/src/main/java/org/xtimms/tokusho/sections/shelf/FavouriteManga.kt new file mode 100644 index 0000000..0e8c78a --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/sections/shelf/FavouriteManga.kt @@ -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, + @Relation( + parentColumn = "manga_id", + entityColumn = "tag_id", + associateBy = Junction(MangaTagsEntity::class) + ) + val tags: List +) \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/sections/shelf/ShelfTabs.kt b/app/src/main/java/org/xtimms/tokusho/sections/shelf/ShelfTabs.kt index 8b782c6..24424b6 100644 --- a/app/src/main/java/org/xtimms/tokusho/sections/shelf/ShelfTabs.kt +++ b/app/src/main/java/org/xtimms/tokusho/sections/shelf/ShelfTabs.kt @@ -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, + categories: List, 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), ) }, diff --git a/app/src/main/java/org/xtimms/tokusho/sections/shelf/ShelfUiState.kt b/app/src/main/java/org/xtimms/tokusho/sections/shelf/ShelfUiState.kt new file mode 100644 index 0000000..8466d85 --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/sections/shelf/ShelfUiState.kt @@ -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 = 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) +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/sections/shelf/ShelfView.kt b/app/src/main/java/org/xtimms/tokusho/sections/shelf/ShelfView.kt index fb8750d..a2515ce 100644 --- a/app/src/main/java/org/xtimms/tokusho/sections/shelf/ShelfView.kt +++ b/app/src/main/java/org/xtimms/tokusho/sections/shelf/ShelfView.kt @@ -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, currentPage: () -> Int, showPageTabs: Boolean, - getNumberOfMangaForCategory: (ShelfCategory) -> Int?, + getNumberOfMangaForCategory: (FavouriteCategory) -> Int?, getLibraryForPage: (Int) -> List, 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, + uiState: ShelfUiState, currentPage: () -> Int, showPageTabs: Boolean, - getNumberOfMangaForCategory: (ShelfCategory) -> Int?, + getNumberOfMangaForCategory: (FavouriteCategory) -> Int?, getLibraryForPage: (Int) -> List, topBarHeightPx: Float, topBarOffsetY: Animatable = 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> \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/sections/shelf/ShelfViewModel.kt b/app/src/main/java/org/xtimms/tokusho/sections/shelf/ShelfViewModel.kt new file mode 100644 index 0000000..8e78633 --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/sections/shelf/ShelfViewModel.kt @@ -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() { + + 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()) + +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/utils/ReversibleHandle.kt b/app/src/main/java/org/xtimms/tokusho/utils/ReversibleHandle.kt new file mode 100644 index 0000000..0f1d11a --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/utils/ReversibleHandle.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/utils/lang/Flow.kt b/app/src/main/java/org/xtimms/tokusho/utils/lang/Flow.kt index 287c15f..d668e5a 100644 --- a/app/src/main/java/org/xtimms/tokusho/utils/lang/Flow.kt +++ b/app/src/main/java/org/xtimms/tokusho/utils/lang/Flow.kt @@ -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 @@ -13,4 +14,8 @@ fun Flow.onEachWhile(action: suspend (T) -> Boolean): Flow { }.onCompletion { isCalled = false } +} + +inline fun Flow>.mapItems(crossinline transform: (T) -> R): Flow> { + return map { list -> list.map(transform) } } \ No newline at end of file diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml index 4c62d31..36efbd3 100644 --- a/app/src/main/res/values/plurals.xml +++ b/app/src/main/res/values/plurals.xml @@ -8,4 +8,8 @@ %1$d minute %1$d minutes + + %1$d category + %1$d categories + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cc1716c..23ba5a7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -118,4 +118,10 @@ Total chapters read Minimum chapters read Maximum chapters read + Read later + Categories, new chapters updates + Categories + Edit categories + Updates + Add \ No newline at end of file