diff --git a/app/src/main/java/eu/kanade/data/category/CategoryRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/category/CategoryRepositoryImpl.kt index a10831222..519960b19 100644 --- a/app/src/main/java/eu/kanade/data/category/CategoryRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/category/CategoryRepositoryImpl.kt @@ -5,7 +5,7 @@ import eu.kanade.data.listOfLongsAdapter import eu.kanade.domain.category.model.Category import eu.kanade.domain.category.model.CategoryUpdate import eu.kanade.domain.category.repository.CategoryRepository -import eu.kanade.domain.category.repository.DuplicateNameException +import eu.kanade.tachiyomi.Database import kotlinx.coroutines.flow.Flow class CategoryRepositoryImpl( @@ -33,16 +33,14 @@ class CategoryRepositoryImpl( } // SY --> - @Throws(DuplicateNameException::class) - override suspend fun insert(name: String, order: Long): Long { - if (checkDuplicateName(name)) throw DuplicateNameException(name) + override suspend fun insert(category: Category): Long { return handler.awaitOne(true) { categoriesQueries.insert( - name = name, - order = order, - flags = 0L, + name = category.name, + order = category.order, + flags = category.flags, // SY --> - mangaOrder = emptyList(), + mangaOrder = category.mangaOrder, // SY <-- ) categoriesQueries.selectLastInsertedRowId() @@ -50,22 +48,32 @@ class CategoryRepositoryImpl( } // SY <-- - @Throws(DuplicateNameException::class) - override suspend fun update(payload: CategoryUpdate) { - if (payload.name != null && checkDuplicateName(payload.name)) throw DuplicateNameException(payload.name) + override suspend fun updatePartial(update: CategoryUpdate) { handler.await { - categoriesQueries.update( - name = payload.name, - order = payload.order, - flags = payload.flags, - categoryId = payload.id, - // SY --> - mangaOrder = payload.mangaOrder?.let(listOfLongsAdapter::encode), - // SY <-- - ) + updatePartialBlocking(update) } } + override suspend fun updatePartial(updates: List) { + handler.await(true) { + for (update in updates) { + updatePartialBlocking(update) + } + } + } + + private fun Database.updatePartialBlocking(update: CategoryUpdate) { + categoriesQueries.update( + name = update.name, + order = update.order, + flags = update.flags, + categoryId = update.id, + // SY --> + mangaOrder = update.mangaOrder?.let(listOfLongsAdapter::encode), + // SY <-- + ) + } + override suspend fun delete(categoryId: Long) { handler.await { categoriesQueries.delete( @@ -73,10 +81,4 @@ class CategoryRepositoryImpl( ) } } - - override suspend fun checkDuplicateName(name: String): Boolean { - return handler - .awaitList { categoriesQueries.getCategories() } - .any { it.name == name } - } } diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index e782379ed..e0e0ad793 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -6,9 +6,11 @@ import eu.kanade.data.history.HistoryRepositoryImpl import eu.kanade.data.manga.MangaRepositoryImpl import eu.kanade.data.source.SourceRepositoryImpl import eu.kanade.data.track.TrackRepositoryImpl +import eu.kanade.domain.category.interactor.CreateCategoryWithName import eu.kanade.domain.category.interactor.DeleteCategory import eu.kanade.domain.category.interactor.GetCategories -import eu.kanade.domain.category.interactor.InsertCategory +import eu.kanade.domain.category.interactor.RenameCategory +import eu.kanade.domain.category.interactor.ReorderCategory import eu.kanade.domain.category.interactor.SetMangaCategories import eu.kanade.domain.category.interactor.UpdateCategory import eu.kanade.domain.category.repository.CategoryRepository @@ -69,7 +71,9 @@ class DomainModule : InjektModule { override fun InjektRegistrar.registerInjectables() { addSingletonFactory { CategoryRepositoryImpl(get()) } addFactory { GetCategories(get()) } - addFactory { InsertCategory(get()) } + addFactory { CreateCategoryWithName(get()) } + addFactory { RenameCategory(get()) } + addFactory { ReorderCategory(get()) } addFactory { UpdateCategory(get()) } addFactory { DeleteCategory(get()) } diff --git a/app/src/main/java/eu/kanade/domain/SYDomainModule.kt b/app/src/main/java/eu/kanade/domain/SYDomainModule.kt index 3c94925c8..a111f64b5 100644 --- a/app/src/main/java/eu/kanade/domain/SYDomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/SYDomainModule.kt @@ -9,10 +9,12 @@ import eu.kanade.domain.chapter.interactor.DeleteChapters import eu.kanade.domain.chapter.interactor.GetChapterByUrl import eu.kanade.domain.chapter.interactor.GetMergedChapterByMangaId import eu.kanade.domain.history.interactor.GetHistoryByMangaId +import eu.kanade.domain.manga.interactor.CreateSortTag import eu.kanade.domain.manga.interactor.DeleteByMergeId import eu.kanade.domain.manga.interactor.DeleteFavoriteEntries import eu.kanade.domain.manga.interactor.DeleteMangaById import eu.kanade.domain.manga.interactor.DeleteMergeById +import eu.kanade.domain.manga.interactor.DeleteSortTag import eu.kanade.domain.manga.interactor.GetAllManga import eu.kanade.domain.manga.interactor.GetExhFavoriteMangaWithMetadata import eu.kanade.domain.manga.interactor.GetFavoriteEntries @@ -26,9 +28,11 @@ import eu.kanade.domain.manga.interactor.GetMergedReferencesById import eu.kanade.domain.manga.interactor.GetSearchMetadata import eu.kanade.domain.manga.interactor.GetSearchTags import eu.kanade.domain.manga.interactor.GetSearchTitles +import eu.kanade.domain.manga.interactor.GetSortTag import eu.kanade.domain.manga.interactor.InsertFavoriteEntries import eu.kanade.domain.manga.interactor.InsertFlatMetadata import eu.kanade.domain.manga.interactor.InsertMergedReference +import eu.kanade.domain.manga.interactor.ReorderSortTag import eu.kanade.domain.manga.interactor.SetMangaFilteredScanlators import eu.kanade.domain.manga.interactor.UpdateMergedSettings import eu.kanade.domain.manga.repository.FavoritesEntryRepository @@ -36,8 +40,12 @@ import eu.kanade.domain.manga.repository.MangaMergeRepository import eu.kanade.domain.manga.repository.MangaMetadataRepository import eu.kanade.domain.source.interactor.CountFeedSavedSearchBySourceId import eu.kanade.domain.source.interactor.CountFeedSavedSearchGlobal +import eu.kanade.domain.source.interactor.CreateSourceCategory +import eu.kanade.domain.source.interactor.CreateSourceRepo import eu.kanade.domain.source.interactor.DeleteFeedSavedSearchById import eu.kanade.domain.source.interactor.DeleteSavedSearchById +import eu.kanade.domain.source.interactor.DeleteSourceCategory +import eu.kanade.domain.source.interactor.DeleteSourceRepos import eu.kanade.domain.source.interactor.GetExhSavedSearch import eu.kanade.domain.source.interactor.GetFeedSavedSearchBySourceId import eu.kanade.domain.source.interactor.GetFeedSavedSearchGlobal @@ -47,8 +55,10 @@ import eu.kanade.domain.source.interactor.GetSavedSearchBySourceIdFeed import eu.kanade.domain.source.interactor.GetSavedSearchGlobalFeed import eu.kanade.domain.source.interactor.GetShowLatest import eu.kanade.domain.source.interactor.GetSourceCategories +import eu.kanade.domain.source.interactor.GetSourceRepos import eu.kanade.domain.source.interactor.InsertFeedSavedSearch import eu.kanade.domain.source.interactor.InsertSavedSearch +import eu.kanade.domain.source.interactor.RenameSourceCategory import eu.kanade.domain.source.interactor.SetSourceCategories import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver import eu.kanade.domain.source.interactor.ToggleSources @@ -64,7 +74,6 @@ import xyz.nulldev.ts.api.http.serializer.FilterSerializer class SYDomainModule : InjektModule { override fun InjektRegistrar.registerInjectables() { - addFactory { GetSourceCategories(get()) } addFactory { GetShowLatest(get()) } addFactory { ToggleExcludeFromDataSaver(get()) } addFactory { SetSourceCategories(get()) } @@ -77,6 +86,17 @@ class SYDomainModule : InjektModule { addFactory { FilterSerializer() } addFactory { GetHistoryByMangaId(get()) } addFactory { GetChapterByUrl(get()) } + addFactory { CreateSourceRepo(get()) } + addFactory { DeleteSourceRepos(get()) } + addFactory { GetSourceRepos(get()) } + addFactory { GetSourceCategories(get()) } + addFactory { CreateSourceCategory(get()) } + addFactory { RenameSourceCategory(get(), get()) } + addFactory { DeleteSourceCategory(get()) } + addFactory { GetSortTag(get()) } + addFactory { CreateSortTag(get(), get()) } + addFactory { DeleteSortTag(get(), get()) } + addFactory { ReorderSortTag(get(), get()) } addSingletonFactory { MangaMetadataRepositoryImpl(get()) } addFactory { GetFlatMetadataById(get()) } diff --git a/app/src/main/java/eu/kanade/domain/category/interactor/CreateCategoryWithName.kt b/app/src/main/java/eu/kanade/domain/category/interactor/CreateCategoryWithName.kt new file mode 100644 index 000000000..ab0fc0d60 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/category/interactor/CreateCategoryWithName.kt @@ -0,0 +1,47 @@ +package eu.kanade.domain.category.interactor + +import eu.kanade.domain.category.model.Category +import eu.kanade.domain.category.model.anyWithName +import eu.kanade.domain.category.repository.CategoryRepository +import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext +import logcat.LogPriority + +class CreateCategoryWithName( + private val categoryRepository: CategoryRepository, +) { + + suspend fun await(name: String): Result = withContext(NonCancellable) await@{ + val categories = categoryRepository.getAll() + if (categories.anyWithName(name)) { + return@await Result.NameAlreadyExistsError + } + + val nextOrder = categories.maxOfOrNull { it.order }?.plus(1) ?: 0 + val newCategory = Category( + id = 0, + name = name, + order = nextOrder, + flags = 0, + mangaOrder = emptyList(), + ) + + try { + categoryRepository.insert(newCategory) + Result.Success(/* SY --> */newCategory/* SY <-- */) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + Result.InternalError(e) + } + } + + sealed class Result { + // SY --> + data class Success(val category: Category) : Result() + + // SY <-- + object NameAlreadyExistsError : Result() + data class InternalError(val error: Throwable) : Result() + } +} diff --git a/app/src/main/java/eu/kanade/domain/category/interactor/DeleteCategory.kt b/app/src/main/java/eu/kanade/domain/category/interactor/DeleteCategory.kt index f44369ac2..52992fcf5 100644 --- a/app/src/main/java/eu/kanade/domain/category/interactor/DeleteCategory.kt +++ b/app/src/main/java/eu/kanade/domain/category/interactor/DeleteCategory.kt @@ -1,12 +1,43 @@ package eu.kanade.domain.category.interactor +import eu.kanade.domain.category.model.CategoryUpdate import eu.kanade.domain.category.repository.CategoryRepository +import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext +import logcat.LogPriority class DeleteCategory( private val categoryRepository: CategoryRepository, ) { - suspend fun await(categoryId: Long) { - categoryRepository.delete(categoryId) + suspend fun await(categoryId: Long) = withContext(NonCancellable) await@{ + try { + categoryRepository.delete(categoryId) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + return@await Result.InternalError(e) + } + + val categories = categoryRepository.getAll() + val updates = categories.mapIndexed { index, category -> + CategoryUpdate( + id = category.id, + order = index.toLong(), + ) + } + + try { + categoryRepository.updatePartial(updates) + Result.Success + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + Result.InternalError(e) + } + } + + sealed class Result { + object Success : Result() + data class InternalError(val error: Throwable) : Result() } } diff --git a/app/src/main/java/eu/kanade/domain/category/interactor/InsertCategory.kt b/app/src/main/java/eu/kanade/domain/category/interactor/InsertCategory.kt deleted file mode 100644 index 813a7fde0..000000000 --- a/app/src/main/java/eu/kanade/domain/category/interactor/InsertCategory.kt +++ /dev/null @@ -1,26 +0,0 @@ -package eu.kanade.domain.category.interactor - -import eu.kanade.domain.category.repository.CategoryRepository - -class InsertCategory( - private val categoryRepository: CategoryRepository, -) { - - suspend fun await(name: String, order: Long): Result { - return try { - // SY --> - Result.Success(categoryRepository.insert(name, order)) - // SY <-- - } catch (e: Exception) { - Result.Error(e) - } - } - - sealed class Result { - // SY --> - data class Success(val id: Long) : Result() - - // Sy <-- - data class Error(val error: Exception) : Result() - } -} diff --git a/app/src/main/java/eu/kanade/domain/category/interactor/RenameCategory.kt b/app/src/main/java/eu/kanade/domain/category/interactor/RenameCategory.kt new file mode 100644 index 000000000..e2fbc4d7e --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/category/interactor/RenameCategory.kt @@ -0,0 +1,43 @@ +package eu.kanade.domain.category.interactor + +import eu.kanade.domain.category.model.Category +import eu.kanade.domain.category.model.CategoryUpdate +import eu.kanade.domain.category.model.anyWithName +import eu.kanade.domain.category.repository.CategoryRepository +import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext +import logcat.LogPriority + +class RenameCategory( + private val categoryRepository: CategoryRepository, +) { + + suspend fun await(categoryId: Long, name: String) = withContext(NonCancellable) await@{ + val categories = categoryRepository.getAll() + if (categories.anyWithName(name)) { + return@await Result.NameAlreadyExistsError + } + + val update = CategoryUpdate( + id = categoryId, + name = name, + ) + + try { + categoryRepository.updatePartial(update) + Result.Success + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + Result.InternalError(e) + } + } + + suspend fun await(category: Category, name: String) = await(category.id, name) + + sealed class Result { + object Success : Result() + object NameAlreadyExistsError : Result() + data class InternalError(val error: Throwable) : Result() + } +} diff --git a/app/src/main/java/eu/kanade/domain/category/interactor/ReorderCategory.kt b/app/src/main/java/eu/kanade/domain/category/interactor/ReorderCategory.kt new file mode 100644 index 000000000..bfaaf174a --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/category/interactor/ReorderCategory.kt @@ -0,0 +1,51 @@ +package eu.kanade.domain.category.interactor + +import eu.kanade.domain.category.model.Category +import eu.kanade.domain.category.model.CategoryUpdate +import eu.kanade.domain.category.repository.CategoryRepository +import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext +import logcat.LogPriority + +class ReorderCategory( + private val categoryRepository: CategoryRepository, +) { + + suspend fun await(categoryId: Long, newPosition: Int) = withContext(NonCancellable) await@{ + val categories = categoryRepository.getAll() + + val currentIndex = categories.indexOfFirst { it.id == categoryId } + if (currentIndex == newPosition) { + return@await Result.Unchanged + } + + val reorderedCategories = categories.toMutableList() + val reorderedCategory = reorderedCategories.removeAt(currentIndex) + reorderedCategories.add(newPosition, reorderedCategory) + + val updates = reorderedCategories.mapIndexed { index, category -> + CategoryUpdate( + id = category.id, + order = index.toLong(), + ) + } + + try { + categoryRepository.updatePartial(updates) + Result.Success + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + Result.InternalError(e) + } + } + + suspend fun await(category: Category, newPosition: Long): Result = + await(category.id, newPosition.toInt()) + + sealed class Result { + object Success : Result() + object Unchanged : Result() + data class InternalError(val error: Throwable) : Result() + } +} diff --git a/app/src/main/java/eu/kanade/domain/category/interactor/UpdateCategory.kt b/app/src/main/java/eu/kanade/domain/category/interactor/UpdateCategory.kt index bff2a7902..939a87b6b 100644 --- a/app/src/main/java/eu/kanade/domain/category/interactor/UpdateCategory.kt +++ b/app/src/main/java/eu/kanade/domain/category/interactor/UpdateCategory.kt @@ -2,14 +2,16 @@ package eu.kanade.domain.category.interactor import eu.kanade.domain.category.model.CategoryUpdate import eu.kanade.domain.category.repository.CategoryRepository +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext class UpdateCategory( private val categoryRepository: CategoryRepository, ) { - suspend fun await(payload: CategoryUpdate): Result { - return try { - categoryRepository.update(payload) + suspend fun await(payload: CategoryUpdate): Result = withContext(NonCancellable) { + try { + categoryRepository.updatePartial(payload) Result.Success } catch (e: Exception) { Result.Error(e) diff --git a/app/src/main/java/eu/kanade/domain/category/model/Category.kt b/app/src/main/java/eu/kanade/domain/category/model/Category.kt index 7659e0ad2..6cfe61d5d 100644 --- a/app/src/main/java/eu/kanade/domain/category/model/Category.kt +++ b/app/src/main/java/eu/kanade/domain/category/model/Category.kt @@ -41,6 +41,10 @@ data class Category( } } +internal fun List.anyWithName(name: String): Boolean { + return any { name.equals(it.name, ignoreCase = true) } +} + fun Category.toDbCategory(): DbCategory = CategoryImpl().also { it.name = name it.id = id.toInt() diff --git a/app/src/main/java/eu/kanade/domain/category/repository/CategoryRepository.kt b/app/src/main/java/eu/kanade/domain/category/repository/CategoryRepository.kt index 0f69b1a72..e907d411c 100644 --- a/app/src/main/java/eu/kanade/domain/category/repository/CategoryRepository.kt +++ b/app/src/main/java/eu/kanade/domain/category/repository/CategoryRepository.kt @@ -15,16 +15,12 @@ interface CategoryRepository { fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow> // SY --> - @Throws(DuplicateNameException::class) - suspend fun insert(name: String, order: Long): Long + suspend fun insert(category: Category): Long // SY <-- - @Throws(DuplicateNameException::class) - suspend fun update(payload: CategoryUpdate) + suspend fun updatePartial(update: CategoryUpdate) + + suspend fun updatePartial(updates: List) suspend fun delete(categoryId: Long) - - suspend fun checkDuplicateName(name: String): Boolean } - -class DuplicateNameException(name: String) : Exception("There's a category which is named \"$name\" already") diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/CreateSortTag.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/CreateSortTag.kt new file mode 100644 index 000000000..08c90c636 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/CreateSortTag.kt @@ -0,0 +1,40 @@ +package eu.kanade.domain.manga.interactor + +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.preference.plusAssign + +class CreateSortTag( + private val preferences: PreferencesHelper, + private val getSortTag: GetSortTag, +) { + + fun await(tag: String): Result { + // Do not allow duplicate categories. + // Do not allow duplicate categories. + if (tagExists(tag.trim())) { + return Result.TagExists + } + + val size = preferences.sortTagsForLibrary().get().size + + preferences.sortTagsForLibrary() += encodeTag(size, tag) + + return Result.Success + } + + sealed class Result { + object TagExists : Result() + object Success : Result() + } + + /** + * Returns true if a tag with the given name already exists. + */ + private fun tagExists(name: String): Boolean { + return getSortTag.await().any { it.equals(name) } + } + + companion object { + fun encodeTag(index: Int, tag: String) = "$index|${tag.trim()}" + } +} diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/DeleteSortTag.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/DeleteSortTag.kt new file mode 100644 index 000000000..50cd2d7e6 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/DeleteSortTag.kt @@ -0,0 +1,16 @@ +package eu.kanade.domain.manga.interactor + +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.preference.minusAssign + +class DeleteSortTag( + private val preferences: PreferencesHelper, + private val getSortTag: GetSortTag, +) { + + fun await(tag: String) { + getSortTag.await().withIndex().find { it.value == tag }?.let { + preferences.sortTagsForLibrary() -= CreateSortTag.encodeTag(it.index, it.value) + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/GetSortTag.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/GetSortTag.kt new file mode 100644 index 000000000..5e1427fa5 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/GetSortTag.kt @@ -0,0 +1,27 @@ +package eu.kanade.domain.manga.interactor + +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetSortTag(private val preferences: PreferencesHelper) { + + fun subscribe(): Flow> { + return preferences.sortTagsForLibrary().asFlow() + .map(::mapSortTags) + } + + fun await() = getSortTags(preferences).let(::mapSortTags) + + companion object { + fun getSortTags(preferences: PreferencesHelper) = preferences.sortTagsForLibrary().get() + + fun mapSortTags(tags: Set) = tags.mapNotNull { + val index = it.indexOf('|') + if (index != -1) { + (it.substring(0, index).toIntOrNull() ?: return@mapNotNull null) to it.substring(index + 1) + } else null + } + .sortedBy { it.first }.map { it.second } + } +} diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/ReorderSortTag.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/ReorderSortTag.kt new file mode 100644 index 000000000..c175eff4d --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/ReorderSortTag.kt @@ -0,0 +1,40 @@ +package eu.kanade.domain.manga.interactor + +import eu.kanade.tachiyomi.data.preference.PreferencesHelper + +class ReorderSortTag( + private val preferences: PreferencesHelper, + private val getSortTag: GetSortTag, +) { + + fun await(tag: String, newPosition: Int): Result { + val tags = getSortTag.await() + val currentIndex = tags.indexOfFirst { it == tag } + + if (currentIndex == -1) { + return Result.InternalError + } + + if (currentIndex == newPosition) { + return Result.Unchanged + } + + val reorderedTags = tags.toMutableList() + val reorderedTag = reorderedTags.removeAt(currentIndex) + reorderedTags.add(newPosition, reorderedTag) + + preferences.sortTagsForLibrary().set( + reorderedTags.mapIndexed { index, s -> + CreateSortTag.encodeTag(index, s) + }.toSet(), + ) + + return Result.Success + } + + sealed class Result { + object Success : Result() + object Unchanged : Result() + object InternalError : Result() + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/CreateSourceCategory.kt b/app/src/main/java/eu/kanade/domain/source/interactor/CreateSourceCategory.kt new file mode 100644 index 000000000..ad4e9f74d --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/CreateSourceCategory.kt @@ -0,0 +1,36 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.preference.plusAssign + +class CreateSourceCategory(private val preferences: PreferencesHelper) { + + fun await(category: String): Result { + // Do not allow duplicate categories. + if (categoryExists(category)) { + return Result.CategoryExists + } + + if (category.contains("|")) { + return Result.InvalidName + } + + // Create category. + preferences.sourcesTabCategories() += category + + return Result.Success + } + + sealed class Result { + object CategoryExists : Result() + object InvalidName : Result() + object Success : Result() + } + + /** + * Returns true if a repo with the given name already exists. + */ + private fun categoryExists(name: String): Boolean { + return preferences.sourcesTabCategories().get().any { it.equals(name, true) } + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/CreateSourceRepo.kt b/app/src/main/java/eu/kanade/domain/source/interactor/CreateSourceRepo.kt new file mode 100644 index 000000000..cff48d7aa --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/CreateSourceRepo.kt @@ -0,0 +1,40 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.preference.plusAssign + +class CreateSourceRepo(private val preferences: PreferencesHelper) { + + fun await(name: String): Result { + // Do not allow duplicate repos. + if (repoExists(name)) { + return Result.RepoExists + } + + // Do not allow invalid formats + if (!name.matches(repoRegex)) { + return Result.InvalidName + } + + preferences.extensionRepos() += name + + return Result.Success + } + + sealed class Result { + object RepoExists : Result() + object InvalidName : Result() + object Success : Result() + } + + /** + * Returns true if a repo with the given name already exists. + */ + private fun repoExists(name: String): Boolean { + return preferences.extensionRepos().get().any { it.equals(name, true) } + } + + companion object { + val repoRegex = """^[a-zA-Z0-9-_.]*?\/[a-zA-Z0-9-_.]*?$""".toRegex() + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/DeleteSourceCategory.kt b/app/src/main/java/eu/kanade/domain/source/interactor/DeleteSourceCategory.kt new file mode 100644 index 000000000..accb92035 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/DeleteSourceCategory.kt @@ -0,0 +1,16 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.preference.minusAssign + +class DeleteSourceCategory(private val preferences: PreferencesHelper) { + + fun await(category: String) { + preferences.sourcesTabSourcesInCategories().set( + preferences.sourcesTabSourcesInCategories().get() + .filterNot { it.substringAfter("|") == category } + .toSet(), + ) + preferences.sourcesTabCategories() -= category + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/DeleteSourceRepos.kt b/app/src/main/java/eu/kanade/domain/source/interactor/DeleteSourceRepos.kt new file mode 100644 index 000000000..b9ef52e71 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/DeleteSourceRepos.kt @@ -0,0 +1,12 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.tachiyomi.data.preference.PreferencesHelper + +class DeleteSourceRepos(private val preferences: PreferencesHelper) { + + fun await(repos: List) { + preferences.extensionRepos().set( + preferences.extensionRepos().get().filterNot { it in repos }.toSet(), + ) + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceCategories.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceCategories.kt index 589570624..46d81a6c5 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceCategories.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceCategories.kt @@ -2,12 +2,13 @@ package eu.kanade.domain.source.interactor import eu.kanade.tachiyomi.data.preference.PreferencesHelper import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map class GetSourceCategories( private val preferences: PreferencesHelper, ) { - fun subscribe(): Flow> { - return preferences.sourcesTabCategories().asFlow() + fun subscribe(): Flow> { + return preferences.sourcesTabCategories().asFlow().map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) } } } diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceRepos.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceRepos.kt new file mode 100644 index 000000000..e72fe85d4 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceRepos.kt @@ -0,0 +1,12 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetSourceRepos(private val preferences: PreferencesHelper) { + + fun subscribe(): Flow> { + return preferences.extensionRepos().asFlow().map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) } + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/RenameSourceCategory.kt b/app/src/main/java/eu/kanade/domain/source/interactor/RenameSourceCategory.kt new file mode 100644 index 000000000..ea4099875 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/RenameSourceCategory.kt @@ -0,0 +1,35 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.tachiyomi.data.preference.PreferencesHelper + +class RenameSourceCategory( + private val preferences: PreferencesHelper, + private val createSourceCategory: CreateSourceCategory, +) { + + fun await(categoryOld: String, categoryNew: String): CreateSourceCategory.Result { + when (val result = createSourceCategory.await(categoryNew)) { + CreateSourceCategory.Result.CategoryExists -> return result + CreateSourceCategory.Result.InvalidName -> return result + CreateSourceCategory.Result.Success -> {} + } + + preferences.sourcesTabSourcesInCategories().set( + preferences.sourcesTabSourcesInCategories().get() + .map { + val index = it.indexOf('|') + if (index != -1 && it.substring(index + 1) == categoryOld) { + it.substring(0, index + 1) + categoryNew + } else it + } + .toSet(), + ) + preferences.sourcesTabCategories().set( + preferences.sourcesTabCategories().get() + .minus(categoryOld) + .plus(categoryNew), + ) + + return CreateSourceCategory.Result.Success + } +} diff --git a/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt new file mode 100644 index 000000000..402bcb243 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt @@ -0,0 +1,113 @@ +package eu.kanade.presentation.category + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import eu.kanade.presentation.category.components.CategoryContent +import eu.kanade.presentation.category.components.CategoryCreateDialog +import eu.kanade.presentation.category.components.CategoryDeleteDialog +import eu.kanade.presentation.category.components.CategoryFloatingActionButton +import eu.kanade.presentation.category.components.CategoryRenameDialog +import eu.kanade.presentation.category.components.CategoryTopAppBar +import eu.kanade.presentation.components.EmptyScreen +import eu.kanade.presentation.components.Scaffold +import eu.kanade.presentation.util.horizontalPadding +import eu.kanade.presentation.util.plus +import eu.kanade.presentation.util.topPaddingValues +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.category.CategoryPresenter +import eu.kanade.tachiyomi.ui.category.CategoryPresenter.Dialog +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.collectLatest + +@Composable +fun CategoryScreen( + presenter: CategoryPresenter, + navigateUp: () -> Unit, +) { + val lazyListState = rememberLazyListState() + val topAppBarScrollState = rememberTopAppBarScrollState() + val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarScrollState) + Scaffold( + modifier = Modifier + .statusBarsPadding() + .nestedScroll(topAppBarScrollBehavior.nestedScrollConnection), + topBar = { + CategoryTopAppBar( + topAppBarScrollBehavior = topAppBarScrollBehavior, + navigateUp = navigateUp, + title = stringResource(id = R.string.action_edit_categories), + ) + }, + floatingActionButton = { + CategoryFloatingActionButton( + lazyListState = lazyListState, + onCreate = { presenter.dialog = CategoryPresenter.Dialog.Create }, + ) + }, + ) { paddingValues -> + val context = LocalContext.current + val categories by presenter.categories.collectAsState(initial = emptyList()) + if (categories.isEmpty()) { + EmptyScreen(textResource = R.string.information_empty_category) + } else { + CategoryContent( + categories = categories, + lazyListState = lazyListState, + paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding), + onMoveUp = { presenter.moveUp(it) }, + onMoveDown = { presenter.moveDown(it) }, + onRename = { presenter.dialog = Dialog.Rename(it) }, + onDelete = { presenter.dialog = Dialog.Delete(it) }, + ) + } + val onDismissRequest = { presenter.dialog = null } + when (val dialog = presenter.dialog) { + Dialog.Create -> { + CategoryCreateDialog( + onDismissRequest = onDismissRequest, + onCreate = { presenter.createCategory(it) }, + title = stringResource(R.string.action_add_category), + ) + } + is Dialog.Rename -> { + CategoryRenameDialog( + onDismissRequest = onDismissRequest, + onRename = { presenter.renameCategory(dialog.category, it) }, + category = dialog.category.name, + ) + } + is Dialog.Delete -> { + CategoryDeleteDialog( + onDismissRequest = onDismissRequest, + onDelete = { presenter.deleteCategory(dialog.category) }, + title = stringResource(R.string.delete_category), + text = stringResource(R.string.delete_category_confirmation, dialog.category.name), + ) + } + else -> {} + } + LaunchedEffect(Unit) { + presenter.events.collectLatest { event -> + when (event) { + is CategoryPresenter.Event.CategoryWithNameAlreadyExists -> { + context.toast(R.string.error_category_exists) + } + is CategoryPresenter.Event.InternalError -> { + context.toast(R.string.internal_error) + } + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/category/SortTagScreen.kt b/app/src/main/java/eu/kanade/presentation/category/SortTagScreen.kt new file mode 100644 index 000000000..ad02cd264 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/category/SortTagScreen.kt @@ -0,0 +1,105 @@ +package eu.kanade.presentation.category + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import eu.kanade.presentation.category.components.CategoryCreateDialog +import eu.kanade.presentation.category.components.CategoryDeleteDialog +import eu.kanade.presentation.category.components.CategoryFloatingActionButton +import eu.kanade.presentation.category.components.CategoryTopAppBar +import eu.kanade.presentation.category.components.genre.SortTagContent +import eu.kanade.presentation.components.EmptyScreen +import eu.kanade.presentation.components.Scaffold +import eu.kanade.presentation.util.horizontalPadding +import eu.kanade.presentation.util.plus +import eu.kanade.presentation.util.topPaddingValues +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.category.genre.SortTagPresenter +import eu.kanade.tachiyomi.ui.category.genre.SortTagPresenter.Dialog +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.collectLatest + +@Composable +fun SortTagScreen( + presenter: SortTagPresenter, + navigateUp: () -> Unit, +) { + val lazyListState = rememberLazyListState() + val topAppBarScrollState = rememberTopAppBarScrollState() + val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarScrollState) + Scaffold( + modifier = Modifier + .statusBarsPadding() + .nestedScroll(topAppBarScrollBehavior.nestedScrollConnection), + topBar = { + CategoryTopAppBar( + topAppBarScrollBehavior = topAppBarScrollBehavior, + navigateUp = navigateUp, + title = stringResource(id = R.string.action_edit_tags), + ) + }, + floatingActionButton = { + CategoryFloatingActionButton( + lazyListState = lazyListState, + onCreate = { presenter.dialog = Dialog.Create }, + ) + }, + ) { paddingValues -> + val context = LocalContext.current + val tags by presenter.tags.collectAsState(initial = emptyList()) + if (tags.isEmpty()) { + EmptyScreen(textResource = R.string.information_empty_tags) + } else { + SortTagContent( + categories = tags, + lazyListState = lazyListState, + paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding), + onMoveUp = { tag, index -> presenter.moveUp(tag, index) }, + onMoveDown = { tag, index -> presenter.moveDown(tag, index) }, + onDelete = { presenter.dialog = Dialog.Delete(it) }, + ) + } + val onDismissRequest = { presenter.dialog = null } + when (val dialog = presenter.dialog) { + Dialog.Create -> { + CategoryCreateDialog( + onDismissRequest = onDismissRequest, + onCreate = { presenter.createTag(it) }, + title = stringResource(R.string.add_tag), + extraMessage = stringResource(R.string.action_add_tags_message), + ) + } + is Dialog.Delete -> { + CategoryDeleteDialog( + onDismissRequest = onDismissRequest, + onDelete = { presenter.delete(dialog.tag) }, + title = stringResource(R.string.delete_tag), + text = stringResource(R.string.delete_tag_confirmation, dialog.tag), + ) + } + else -> {} + } + LaunchedEffect(Unit) { + presenter.events.collectLatest { event -> + when (event) { + is SortTagPresenter.Event.TagExists -> { + context.toast(R.string.error_tag_exists) + } + is SortTagPresenter.Event.InternalError -> { + context.toast(R.string.internal_error) + } + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/category/SourceCategoryScreen.kt b/app/src/main/java/eu/kanade/presentation/category/SourceCategoryScreen.kt new file mode 100644 index 000000000..b8f514d63 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/category/SourceCategoryScreen.kt @@ -0,0 +1,114 @@ +package eu.kanade.presentation.category + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import eu.kanade.presentation.category.components.CategoryCreateDialog +import eu.kanade.presentation.category.components.CategoryDeleteDialog +import eu.kanade.presentation.category.components.CategoryFloatingActionButton +import eu.kanade.presentation.category.components.CategoryRenameDialog +import eu.kanade.presentation.category.components.CategoryTopAppBar +import eu.kanade.presentation.category.components.sources.SourceCategoryContent +import eu.kanade.presentation.components.EmptyScreen +import eu.kanade.presentation.components.Scaffold +import eu.kanade.presentation.util.horizontalPadding +import eu.kanade.presentation.util.plus +import eu.kanade.presentation.util.topPaddingValues +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryPresenter +import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryPresenter.Dialog +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.collectLatest + +@Composable +fun SourceCategoryScreen( + presenter: SourceCategoryPresenter, + navigateUp: () -> Unit, +) { + val lazyListState = rememberLazyListState() + val topAppBarScrollState = rememberTopAppBarScrollState() + val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarScrollState) + Scaffold( + modifier = Modifier + .statusBarsPadding() + .nestedScroll(topAppBarScrollBehavior.nestedScrollConnection), + topBar = { + CategoryTopAppBar( + topAppBarScrollBehavior = topAppBarScrollBehavior, + navigateUp = navigateUp, + title = stringResource(id = R.string.action_edit_categories), + ) + }, + floatingActionButton = { + CategoryFloatingActionButton( + lazyListState = lazyListState, + onCreate = { presenter.dialog = Dialog.Create }, + ) + }, + ) { paddingValues -> + val context = LocalContext.current + val categories by presenter.categories.collectAsState(initial = emptyList()) + if (categories.isEmpty()) { + EmptyScreen(textResource = R.string.information_empty_category) + } else { + SourceCategoryContent( + categories = categories, + lazyListState = lazyListState, + paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding), + onRename = { presenter.dialog = Dialog.Rename(it) }, + onDelete = { presenter.dialog = Dialog.Delete(it) }, + ) + } + val onDismissRequest = { presenter.dialog = null } + when (val dialog = presenter.dialog) { + Dialog.Create -> { + CategoryCreateDialog( + onDismissRequest = onDismissRequest, + onCreate = { presenter.createCategory(it) }, + title = stringResource(R.string.action_add_category), + ) + } + is Dialog.Rename -> { + CategoryRenameDialog( + onDismissRequest = onDismissRequest, + onRename = { presenter.renameCategory(dialog.category, it) }, + category = dialog.category, + ) + } + is Dialog.Delete -> { + CategoryDeleteDialog( + onDismissRequest = onDismissRequest, + onDelete = { presenter.deleteCategory(dialog.category) }, + title = stringResource(R.string.delete_category), + text = stringResource(R.string.delete_category_confirmation, dialog.category), + ) + } + else -> {} + } + LaunchedEffect(Unit) { + presenter.events.collectLatest { event -> + when (event) { + is SourceCategoryPresenter.Event.CategoryExists -> { + context.toast(R.string.error_category_exists) + } + is SourceCategoryPresenter.Event.InternalError -> { + context.toast(R.string.internal_error) + } + SourceCategoryPresenter.Event.InvalidName -> { + context.toast(R.string.invalid_category_name) + } + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/category/SourceRepoScreen.kt b/app/src/main/java/eu/kanade/presentation/category/SourceRepoScreen.kt new file mode 100644 index 000000000..f9742c7c7 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/category/SourceRepoScreen.kt @@ -0,0 +1,106 @@ +package eu.kanade.presentation.category + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import eu.kanade.presentation.category.components.CategoryCreateDialog +import eu.kanade.presentation.category.components.CategoryDeleteDialog +import eu.kanade.presentation.category.components.CategoryFloatingActionButton +import eu.kanade.presentation.category.components.CategoryTopAppBar +import eu.kanade.presentation.category.components.repo.SourceRepoContent +import eu.kanade.presentation.components.EmptyScreen +import eu.kanade.presentation.components.Scaffold +import eu.kanade.presentation.util.horizontalPadding +import eu.kanade.presentation.util.plus +import eu.kanade.presentation.util.topPaddingValues +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.category.repos.RepoPresenter +import eu.kanade.tachiyomi.ui.category.repos.RepoPresenter.Dialog +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.collectLatest + +@Composable +fun SourceRepoScreen( + presenter: RepoPresenter, + navigateUp: () -> Unit, +) { + val lazyListState = rememberLazyListState() + val topAppBarScrollState = rememberTopAppBarScrollState() + val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarScrollState) + Scaffold( + modifier = Modifier + .statusBarsPadding() + .nestedScroll(topAppBarScrollBehavior.nestedScrollConnection), + topBar = { + CategoryTopAppBar( + topAppBarScrollBehavior = topAppBarScrollBehavior, + navigateUp = navigateUp, + title = stringResource(R.string.action_edit_repos), + ) + }, + floatingActionButton = { + CategoryFloatingActionButton( + lazyListState = lazyListState, + onCreate = { presenter.dialog = Dialog.Create }, + ) + }, + ) { paddingValues -> + val context = LocalContext.current + val repos by presenter.repos.collectAsState(initial = emptyList()) + if (repos.isEmpty()) { + EmptyScreen(textResource = R.string.information_empty_repos) + } else { + SourceRepoContent( + repos = repos, + lazyListState = lazyListState, + paddingValues = paddingValues + topPaddingValues + PaddingValues(horizontal = horizontalPadding), + onDelete = { presenter.dialog = Dialog.Delete(it) }, + ) + } + val onDismissRequest = { presenter.dialog = null } + when (val dialog = presenter.dialog) { + Dialog.Create -> { + CategoryCreateDialog( + onDismissRequest = onDismissRequest, + onCreate = { presenter.createRepo(it) }, + title = stringResource(R.string.action_add_repo), + extraMessage = stringResource(R.string.action_add_repo_message), + ) + } + is Dialog.Delete -> { + CategoryDeleteDialog( + onDismissRequest = onDismissRequest, + onDelete = { presenter.deleteRepos(listOf(dialog.repo)) }, + title = stringResource(R.string.delete_repo), + text = stringResource(R.string.delete_repo_confirmation, dialog.repo), + ) + } + else -> {} + } + LaunchedEffect(Unit) { + presenter.events.collectLatest { event -> + when (event) { + is RepoPresenter.Event.RepoExists -> { + context.toast(R.string.error_repo_exists) + } + is RepoPresenter.Event.InternalError -> { + context.toast(R.string.internal_error) + } + is RepoPresenter.Event.InvalidName -> { + context.toast(R.string.invalid_repo_name) + } + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryContent.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryContent.kt new file mode 100644 index 000000000..3b61957b8 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryContent.kt @@ -0,0 +1,39 @@ +package eu.kanade.presentation.category.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import eu.kanade.domain.category.model.Category + +@Composable +fun CategoryContent( + categories: List, + lazyListState: LazyListState, + paddingValues: PaddingValues, + onMoveUp: (Category) -> Unit, + onMoveDown: (Category) -> Unit, + onRename: (Category) -> Unit, + onDelete: (Category) -> Unit, +) { + LazyColumn( + state = lazyListState, + contentPadding = paddingValues, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + itemsIndexed(categories) { index, category -> + CategoryListItem( + category = category, + canMoveUp = index != 0, + canMoveDown = index != categories.lastIndex, + onMoveUp = onMoveUp, + onMoveDown = onMoveDown, + onRename = onRename, + onDelete = onDelete, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt new file mode 100644 index 000000000..cc6f99ab1 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt @@ -0,0 +1,129 @@ +package eu.kanade.presentation.category.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import eu.kanade.presentation.components.TextButton +import eu.kanade.tachiyomi.R + +@Composable +fun CategoryCreateDialog( + onDismissRequest: () -> Unit, + onCreate: (String) -> Unit, + // SY --> + title: String, + extraMessage: String? = null, + // SY <-- +) { + val (name, onNameChange) = remember { mutableStateOf("") } + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = { + onCreate(name) + onDismissRequest() + },) { + Text(text = stringResource(id = R.string.action_add)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(id = R.string.action_cancel)) + } + }, + title = { + Text(text = title) + }, + text = { + // SY --> + Column { + if (extraMessage != null) { + Text(extraMessage) + } + // SY <-- + OutlinedTextField( + value = name, + onValueChange = onNameChange, + label = { + Text(text = stringResource(id = R.string.name)) + }, + ) + // SY --> + } + // SY <-- + }, + ) +} + +@Composable +fun CategoryRenameDialog( + onDismissRequest: () -> Unit, + onRename: (String) -> Unit, + category: String, +) { + val (name, onNameChange) = remember { mutableStateOf(category) } + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = { + onRename(name) + onDismissRequest() + },) { + Text(text = stringResource(id = android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(id = R.string.action_cancel)) + } + }, + title = { + Text(text = stringResource(id = R.string.action_rename_category)) + }, + text = { + OutlinedTextField( + value = name, + onValueChange = onNameChange, + label = { + Text(text = stringResource(id = R.string.name)) + }, + ) + }, + ) +} + +@Composable +fun CategoryDeleteDialog( + onDismissRequest: () -> Unit, + onDelete: () -> Unit, + title: String, + text: String, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(R.string.no)) + } + }, + dismissButton = { + TextButton(onClick = { + onDelete() + onDismissRequest() + },) { + Text(text = stringResource(R.string.yes)) + } + }, + title = { + Text(text = title) + }, + text = { + Text(text = text) + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt new file mode 100644 index 000000000..e2c5d8762 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt @@ -0,0 +1,30 @@ +package eu.kanade.presentation.category.components + +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add +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 eu.kanade.presentation.components.ExtendedFloatingActionButton +import eu.kanade.presentation.util.isScrolledToEnd +import eu.kanade.presentation.util.isScrollingUp +import eu.kanade.tachiyomi.R + +@Composable +fun CategoryFloatingActionButton( + lazyListState: LazyListState, + onCreate: () -> Unit, +) { + ExtendedFloatingActionButton( + text = { Text(text = stringResource(id = R.string.action_add)) }, + icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = "") }, + onClick = onCreate, + modifier = Modifier + .navigationBarsPadding(), + expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(), + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryListItem.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryListItem.kt new file mode 100644 index 000000000..ff0ed8807 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryListItem.kt @@ -0,0 +1,63 @@ +package eu.kanade.presentation.category.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowDropDown +import androidx.compose.material.icons.outlined.ArrowDropUp +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.outlined.Label +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import eu.kanade.domain.category.model.Category +import eu.kanade.presentation.util.horizontalPadding + +@Composable +fun CategoryListItem( + category: Category, + canMoveUp: Boolean, + canMoveDown: Boolean, + onMoveUp: (Category) -> Unit, + onMoveDown: (Category) -> Unit, + onRename: (Category) -> Unit, + onDelete: (Category) -> Unit, +) { + ElevatedCard { + Row( + modifier = Modifier + .padding(start = horizontalPadding, top = horizontalPadding, end = horizontalPadding), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(imageVector = Icons.Outlined.Label, contentDescription = "") + Text(text = category.name, modifier = Modifier.padding(start = horizontalPadding)) + } + Row { + IconButton( + onClick = { onMoveUp(category) }, + enabled = canMoveUp, + ) { + Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = "") + } + IconButton( + onClick = { onMoveDown(category) }, + enabled = canMoveDown, + ) { + Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = "") + } + Spacer(modifier = Modifier.weight(1f)) + IconButton(onClick = { onRename(category) }) { + Icon(imageVector = Icons.Outlined.Edit, contentDescription = "") + } + IconButton(onClick = { onDelete(category) }) { + Icon(imageVector = Icons.Outlined.Delete, contentDescription = "") + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryTopAppBar.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryTopAppBar.kt new file mode 100644 index 000000000..1973ff609 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryTopAppBar.kt @@ -0,0 +1,34 @@ +package eu.kanade.presentation.category.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SmallTopAppBar +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import eu.kanade.tachiyomi.R + +@Composable +fun CategoryTopAppBar( + topAppBarScrollBehavior: TopAppBarScrollBehavior, + navigateUp: () -> Unit, + title: String, +) { + SmallTopAppBar( + navigationIcon = { + IconButton(onClick = navigateUp) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = stringResource(R.string.abc_action_bar_up_description), + ) + } + }, + title = { + Text(text = title) + }, + scrollBehavior = topAppBarScrollBehavior, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/category/components/genre/SortTagContent.kt b/app/src/main/java/eu/kanade/presentation/category/components/genre/SortTagContent.kt new file mode 100644 index 000000000..2427bb226 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/category/components/genre/SortTagContent.kt @@ -0,0 +1,37 @@ +package eu.kanade.presentation.category.components.genre + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp + +@Composable +fun SortTagContent( + categories: List, + lazyListState: LazyListState, + paddingValues: PaddingValues, + onMoveUp: (String, Int) -> Unit, + onMoveDown: (String, Int) -> Unit, + onDelete: (String) -> Unit, +) { + LazyColumn( + state = lazyListState, + contentPadding = paddingValues, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + itemsIndexed(categories) { index, tag -> + SortTagListItem( + tag = tag, + index = index, + canMoveUp = index != 0, + canMoveDown = index != categories.lastIndex, + onMoveUp = onMoveUp, + onMoveDown = onMoveDown, + onDelete = onDelete, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/category/components/genre/SortTagListItem.kt b/app/src/main/java/eu/kanade/presentation/category/components/genre/SortTagListItem.kt new file mode 100644 index 000000000..18cef464a --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/category/components/genre/SortTagListItem.kt @@ -0,0 +1,58 @@ +package eu.kanade.presentation.category.components.genre + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowDropDown +import androidx.compose.material.icons.outlined.ArrowDropUp +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Label +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import eu.kanade.presentation.util.horizontalPadding + +@Composable +fun SortTagListItem( + tag: String, + index: Int, + canMoveUp: Boolean, + canMoveDown: Boolean, + onMoveUp: (String, Int) -> Unit, + onMoveDown: (String, Int) -> Unit, + onDelete: (String) -> Unit, +) { + ElevatedCard { + Row( + modifier = Modifier + .padding(start = horizontalPadding, top = horizontalPadding, end = horizontalPadding), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(imageVector = Icons.Outlined.Label, contentDescription = "") + Text(text = tag, modifier = Modifier.padding(start = horizontalPadding)) + } + Row { + IconButton( + onClick = { onMoveUp(tag, index) }, + enabled = canMoveUp, + ) { + Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = "") + } + IconButton( + onClick = { onMoveDown(tag, index) }, + enabled = canMoveDown, + ) { + Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = "") + } + Spacer(modifier = Modifier.weight(1f)) + IconButton(onClick = { onDelete(tag) }) { + Icon(imageVector = Icons.Outlined.Delete, contentDescription = "") + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/category/components/repo/SourceRepoContent.kt b/app/src/main/java/eu/kanade/presentation/category/components/repo/SourceRepoContent.kt new file mode 100644 index 000000000..b8690e714 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/category/components/repo/SourceRepoContent.kt @@ -0,0 +1,30 @@ +package eu.kanade.presentation.category.components.repo + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp + +@Composable +fun SourceRepoContent( + repos: List, + lazyListState: LazyListState, + paddingValues: PaddingValues, + onDelete: (String) -> Unit, +) { + LazyColumn( + state = lazyListState, + contentPadding = paddingValues, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(repos) { repo -> + SourceRepoListItem( + repo = repo, + onDelete = onDelete, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/category/components/repo/SourceRepoListItem.kt b/app/src/main/java/eu/kanade/presentation/category/components/repo/SourceRepoListItem.kt new file mode 100644 index 000000000..c5b223324 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/category/components/repo/SourceRepoListItem.kt @@ -0,0 +1,39 @@ +package eu.kanade.presentation.category.components.repo + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Label +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import eu.kanade.presentation.util.horizontalPadding + +@Composable +fun SourceRepoListItem( + repo: String, + onDelete: (String) -> Unit, +) { + ElevatedCard { + Row( + modifier = Modifier + .padding(start = horizontalPadding, top = horizontalPadding, end = horizontalPadding), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(imageVector = Icons.Outlined.Label, contentDescription = "") + Text(text = repo, modifier = Modifier.padding(start = horizontalPadding)) + } + Row { + Spacer(modifier = Modifier.weight(1f)) + IconButton(onClick = { onDelete(repo) }) { + Icon(imageVector = Icons.Outlined.Delete, contentDescription = "") + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/category/components/sources/SourceCategoryContent.kt b/app/src/main/java/eu/kanade/presentation/category/components/sources/SourceCategoryContent.kt new file mode 100644 index 000000000..fced53a28 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/category/components/sources/SourceCategoryContent.kt @@ -0,0 +1,32 @@ +package eu.kanade.presentation.category.components.sources + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp + +@Composable +fun SourceCategoryContent( + categories: List, + lazyListState: LazyListState, + paddingValues: PaddingValues, + onRename: (String) -> Unit, + onDelete: (String) -> Unit, +) { + LazyColumn( + state = lazyListState, + contentPadding = paddingValues, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + itemsIndexed(categories) { index, category -> + SourceCategoryListItem( + category = category, + onRename = onRename, + onDelete = onDelete, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/category/components/sources/SourceCategoryListItem.kt b/app/src/main/java/eu/kanade/presentation/category/components/sources/SourceCategoryListItem.kt new file mode 100644 index 000000000..adad511f2 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/category/components/sources/SourceCategoryListItem.kt @@ -0,0 +1,44 @@ +package eu.kanade.presentation.category.components.sources + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.outlined.Label +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import eu.kanade.presentation.util.horizontalPadding + +@Composable +fun SourceCategoryListItem( + category: String, + onRename: (String) -> Unit, + onDelete: (String) -> Unit, +) { + ElevatedCard { + Row( + modifier = Modifier + .padding(start = horizontalPadding, top = horizontalPadding, end = horizontalPadding), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(imageVector = Icons.Outlined.Label, contentDescription = "") + Text(text = category, modifier = Modifier.padding(start = horizontalPadding)) + } + Row { + Spacer(modifier = Modifier.weight(1f)) + IconButton(onClick = { onRename(category) }) { + Icon(imageVector = Icons.Outlined.Edit, contentDescription = "") + } + IconButton(onClick = { onDelete(category) }) { + Icon(imageVector = Icons.Outlined.Delete, contentDescription = "") + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/components/ChapterDownloadIndicator.kt b/app/src/main/java/eu/kanade/presentation/components/ChapterDownloadIndicator.kt index b8967ee84..11eb812e0 100644 --- a/app/src/main/java/eu/kanade/presentation/components/ChapterDownloadIndicator.kt +++ b/app/src/main/java/eu/kanade/presentation/components/ChapterDownloadIndicator.kt @@ -60,8 +60,8 @@ fun ChapterDownloadIndicator( }, ) { val indicatorModifier = Modifier - .size(IndicatorSize) - .padding(IndicatorPadding) + .size(IndicatorSize) + .padding(IndicatorPadding) if (isDownloaded) { Icon( imageVector = Icons.Default.CheckCircle, @@ -151,5 +151,6 @@ fun ChapterDownloadIndicator( private val IndicatorSize = 26.dp private val IndicatorPadding = 2.dp + // To match composable parameter name when used later private val IndicatorStrokeWidth = IndicatorPadding diff --git a/app/src/main/java/eu/kanade/presentation/util/Constants.kt b/app/src/main/java/eu/kanade/presentation/util/Constants.kt index da958b5d6..06b9c1214 100644 --- a/app/src/main/java/eu/kanade/presentation/util/Constants.kt +++ b/app/src/main/java/eu/kanade/presentation/util/Constants.kt @@ -3,6 +3,12 @@ package eu.kanade.presentation.util import androidx.compose.foundation.layout.PaddingValues import androidx.compose.ui.unit.dp -val horizontalPadding = 16.dp +private val horizontal = 16.dp -val topPaddingValues = PaddingValues(top = 8.dp) +private val vertical = 8.dp + +val horizontalPadding = horizontal + +val verticalPadding = vertical + +val topPaddingValues = PaddingValues(top = vertical) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt index 9b932e461..a6dc027da 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt @@ -230,8 +230,8 @@ class MergedSource : HttpSource() { val id = insertManga.await( Manga.create().copy( source = mangaSourceId, - url = mangaUrl - ) + url = mangaUrl, + ), )!! val newManga = getManga.await(id)!! updateManga.awaitUpdateFromSource(newManga, source.getMangaDetails(newManga.toMangaInfo()), false) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt index 93f01f09f..b67d9cabc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt @@ -59,7 +59,7 @@ class SourcesPresenter( // SY <-- } - private fun collectLatestSources(sources: List, categories: Set, showLatest: Boolean, showPin: Boolean) { + private fun collectLatestSources(sources: List, categories: List, showLatest: Boolean, showPin: Boolean) { val map = TreeMap> { d1, d2 -> // Sources without a lang defined will be placed at the end when { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt deleted file mode 100755 index b9510868a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt +++ /dev/null @@ -1,42 +0,0 @@ -package eu.kanade.tachiyomi.ui.category - -import eu.davidea.flexibleadapter.FlexibleAdapter - -/** - * Custom adapter for categories. - * - * @param controller The containing controller. - */ -class CategoryAdapter(controller: CategoryController) : - FlexibleAdapter(null, controller, true) { - - /** - * Listener called when an item of the list is released. - */ - val onItemReleaseListener: OnItemReleaseListener = controller - - /** - * Clears the active selections from the list and the model. - */ - override fun clearSelection() { - super.clearSelection() - (0 until itemCount).forEach { getItem(it)?.isSelected = false } - } - - /** - * Toggles the selection of the given position. - * - * @param position The position to toggle. - */ - override fun toggleSelection(position: Int) { - super.toggleSelection(position) - getItem(position)?.isSelected = isSelected(position) - } - - interface OnItemReleaseListener { - /** - * Called when an item of the list is released. - */ - fun onItemReleased(position: Int) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt index c32ae9167..c905db5f3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt @@ -1,357 +1,18 @@ package eu.kanade.tachiyomi.ui.category -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuItem -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ActionMode -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton -import com.google.android.material.snackbar.Snackbar -import dev.chrisbanes.insetter.applyInsetter -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.davidea.flexibleadapter.helpers.UndoHelper -import eu.kanade.domain.category.model.Category -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.CategoriesControllerBinding -import eu.kanade.tachiyomi.ui.base.controller.FabController -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.shrinkOnScroll -import kotlinx.coroutines.launch +import androidx.compose.runtime.Composable +import eu.kanade.presentation.category.CategoryScreen +import eu.kanade.tachiyomi.ui.base.controller.FullComposeController -/** - * Controller to manage the categories for the users' library. - */ -class CategoryController : - NucleusController(), - FabController, - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - CategoryAdapter.OnItemReleaseListener, - CategoryCreateDialog.Listener, - CategoryRenameDialog.Listener, - UndoHelper.OnActionListener { +class CategoryController : FullComposeController() { - /** - * Object used to show ActionMode toolbar. - */ - private var actionMode: ActionMode? = null - - /** - * Adapter containing category items. - */ - private var adapter: CategoryAdapter? = null - - private var actionFab: ExtendedFloatingActionButton? = null - private var actionFabScrollListener: RecyclerView.OnScrollListener? = null - - /** - * Undo helper used for restoring a deleted category. - */ - private var undoHelper: UndoHelper? = null - - /** - * Creates the presenter for this controller. Not to be manually called. - */ override fun createPresenter() = CategoryPresenter() - /** - * Returns the toolbar title to show when this controller is attached. - */ - override fun getTitle(): String? { - return resources?.getString(R.string.action_edit_categories) - } - - override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater) - - /** - * Called after view inflation. Used to initialize the view. - * - * @param view The view of this controller. - */ - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - binding.recycler.applyInsetter { - type(navigationBars = true) { - padding() - } - } - - adapter = CategoryAdapter(this@CategoryController) - binding.recycler.layoutManager = LinearLayoutManager(view.context) - binding.recycler.setHasFixedSize(true) - binding.recycler.adapter = adapter - adapter?.isHandleDragEnabled = true - adapter?.isPermanentDelete = false - - actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler) - - viewScope.launch { - presenter.categories.collect { - setCategories(it.map(::CategoryItem)) - } - } - } - - override fun configureFab(fab: ExtendedFloatingActionButton) { - actionFab = fab - fab.setText(R.string.action_add) - fab.setIconResource(R.drawable.ic_add_24dp) - fab.setOnClickListener { - CategoryCreateDialog(this@CategoryController).showDialog(router, null) - } - } - - override fun cleanupFab(fab: ExtendedFloatingActionButton) { - fab.setOnClickListener(null) - actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) } - actionFab = null - } - - /** - * Called when the view is being destroyed. Used to release references and remove callbacks. - * - * @param view The view of this controller. - */ - override fun onDestroyView(view: View) { - // Manually call callback to delete categories if required - undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL) - undoHelper = null - actionMode = null - adapter = null - super.onDestroyView(view) - } - - /** - * Called from the presenter when the categories are updated. - * - * @param categories The new list of categories to display. - */ - fun setCategories(categories: List) { - actionMode?.finish() - adapter?.updateDataSet(categories) - if (categories.isNotEmpty()) { - binding.emptyView.hide() - val selected = categories.filter { it.isSelected } - if (selected.isNotEmpty()) { - selected.forEach { onItemLongClick(categories.indexOf(it)) } - } - } else { - binding.emptyView.show(R.string.information_empty_category) - } - } - - /** - * Called when action mode is first created. The menu supplied will be used to generate action - * buttons for the action mode. - * - * @param mode ActionMode being created. - * @param menu Menu used to populate action buttons. - * @return true if the action mode should be created, false if entering this mode should be - * aborted. - */ - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - // Inflate menu. - mode.menuInflater.inflate(R.menu.category_selection, menu) - // Enable adapter multi selection. - adapter?.mode = SelectableAdapter.Mode.MULTI - return true - } - - /** - * Called to refresh an action mode's action menu whenever it is invalidated. - * - * @param mode ActionMode being prepared. - * @param menu Menu used to populate action buttons. - * @return true if the menu or action mode was updated, false otherwise. - */ - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val adapter = adapter ?: return false - val count = adapter.selectedItemCount - mode.title = count.toString() - - // Show edit button only when one item is selected - val editItem = mode.menu.findItem(R.id.action_edit) - editItem.isVisible = count == 1 - return true - } - - /** - * Called to report a user click on an action button. - * - * @param mode The current ActionMode. - * @param item The item that was clicked. - * @return true if this callback handled the event, false if the standard MenuItem invocation - * should continue. - */ - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - val adapter = adapter ?: return false - - when (item.itemId) { - R.id.action_delete -> { - undoHelper = UndoHelper(adapter, this) - undoHelper?.start( - adapter.selectedPositions, - (activity as? MainActivity)?.binding?.rootCoordinator!!, - R.string.snack_categories_deleted, - R.string.action_undo, - 4000, - ) - - mode.finish() - } - R.id.action_edit -> { - // Edit selected category - if (adapter.selectedItemCount == 1) { - val position = adapter.selectedPositions.first() - val category = adapter.getItem(position)?.category - if (category != null) { - editCategory(category) - } - } - } - else -> return false - } - return true - } - - /** - * Called when an action mode is about to be exited and destroyed. - * - * @param mode The current ActionMode being destroyed. - */ - override fun onDestroyActionMode(mode: ActionMode) { - // Reset adapter to single selection - adapter?.mode = SelectableAdapter.Mode.IDLE - adapter?.clearSelection() - actionMode = null - } - - /** - * Called when an item in the list is clicked. - * - * @param position The position of the clicked item. - * @return true if this click should enable selection mode. - */ - override fun onItemClick(view: View, position: Int): Boolean { - // Check if action mode is initialized and selected item exist. - return if (actionMode != null && position != RecyclerView.NO_POSITION) { - toggleSelection(position) - true - } else { - false - } - } - - /** - * Called when an item in the list is long clicked. - * - * @param position The position of the clicked item. - */ - override fun onItemLongClick(position: Int) { - val activity = activity as? AppCompatActivity ?: return - - // Check if action mode is initialized. - if (actionMode == null) { - // Initialize action mode - actionMode = activity.startSupportActionMode(this) - } - - // Set item as selected - toggleSelection(position) - } - - /** - * Toggle the selection state of an item. - * If the item was the last one in the selection and is unselected, the ActionMode is finished. - * - * @param position The position of the item to toggle. - */ - private fun toggleSelection(position: Int) { - val adapter = adapter ?: return - - // Mark the position selected - adapter.toggleSelection(position) - - if (adapter.selectedItemCount == 0) { - actionMode?.finish() - } else { - actionMode?.invalidate() - } - } - - /** - * Called when an item is released from a drag. - * - * @param position The position of the released item. - */ - override fun onItemReleased(position: Int) { - val adapter = adapter ?: return - val categories = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.category } - presenter.reorderCategories(categories) - } - - /** - * Called when the undo action is clicked in the snackbar. - * - * @param action The action performed. - */ - override fun onActionCanceled(action: Int, positions: MutableList?) { - adapter?.restoreDeletedItems() - undoHelper = null - } - - /** - * Called when the time to restore the items expires. - * - * @param action The action performed. - * @param event The event that triggered the action - */ - override fun onActionConfirmed(action: Int, event: Int) { - val adapter = adapter ?: return - presenter.deleteCategories(adapter.deletedItems.map { it.category }) - undoHelper = null - } - - /** - * Show a dialog to let the user change the category name. - * - * @param category The category to be edited. - */ - private fun editCategory(category: Category) { - CategoryRenameDialog(this, category).showDialog(router) - } - - /** - * Renames the given category with the given name. - * - * @param category The category to rename. - * @param name The new name of the category. - */ - override fun renameCategory(category: Category, name: String) { - presenter.renameCategory(category, name) - } - - /** - * Creates a new category with the given name. - * - * @param name The name of the new category. - */ - override fun createCategory(name: String) { - presenter.createCategory(name) - } - - /** - * Called from the presenter when a category with the given name already exists. - */ - fun onCategoryExistsError() { - activity?.toast(R.string.error_category_exists) + @Composable + override fun ComposeContent() { + CategoryScreen( + presenter = presenter, + navigateUp = router::popCurrentController, + ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryCreateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryCreateDialog.kt deleted file mode 100644 index 8134ee501..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryCreateDialog.kt +++ /dev/null @@ -1,48 +0,0 @@ -package eu.kanade.tachiyomi.ui.category - -import android.app.Dialog -import android.os.Bundle -import com.bluelinelabs.conductor.Controller -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput - -/** - * Dialog to create a new category for the library. - */ -class CategoryCreateDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T : CategoryCreateDialog.Listener { - - /** - * Name of the new category. Value updated with each input from the user. - */ - private var currentName = "" - - constructor(target: T) : this() { - targetController = target - } - - /** - * Called when creating the dialog for this controller. - * - * @param savedViewState The saved state of this dialog. - * @return a new dialog instance. - */ - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.action_add_category) - .setTextInput(prefill = currentName) { - currentName = it - } - .setPositiveButton(android.R.string.ok) { _, _ -> - (targetController as? Listener)?.createCategory(currentName) - } - .setNegativeButton(android.R.string.cancel, null) - .create() - } - - interface Listener { - fun createCategory(name: String) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt deleted file mode 100755 index 9005229c5..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt +++ /dev/null @@ -1,49 +0,0 @@ -package eu.kanade.tachiyomi.ui.category - -import android.view.View -import androidx.recyclerview.widget.ItemTouchHelper -import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.domain.category.model.Category -import eu.kanade.tachiyomi.databinding.CategoriesItemBinding - -/** - * Holder used to display category items. - * - * @param view The view used by category items. - * @param adapter The adapter containing this holder. - */ -class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHolder(view, adapter) { - - private val binding = CategoriesItemBinding.bind(view) - - init { - setDragHandleView(binding.reorder) - } - - /** - * Binds this holder with the given category. - * - * @param category The category to bind. - */ - fun bind(category: Category) { - binding.title.text = category.name - } - - /** - * Called when an item is released. - * - * @param position The position of the released item. - */ - override fun onItemReleased(position: Int) { - super.onItemReleased(position) - adapter.onItemReleaseListener.onItemReleased(position) - binding.container.isDragged = false - } - - override fun onActionStateChanged(position: Int, actionState: Int) { - super.onActionStateChanged(position, actionState) - if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { - binding.container.isDragged = true - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt deleted file mode 100755 index 7b6b3bf34..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt +++ /dev/null @@ -1,73 +0,0 @@ -package eu.kanade.tachiyomi.ui.category - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.domain.category.model.Category -import eu.kanade.tachiyomi.R - -/** - * Category item for a recycler view. - */ -class CategoryItem(val category: Category) : AbstractFlexibleItem() { - - /** - * Whether this item is currently selected. - */ - var isSelected = false - - /** - * Returns the layout resource for this item. - */ - override fun getLayoutRes(): Int { - return R.layout.categories_item - } - - /** - * Returns a new view holder for this item. - * - * @param view The view of this item. - * @param adapter The adapter of this item. - */ - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): CategoryHolder { - return CategoryHolder(view, adapter as CategoryAdapter) - } - - /** - * Binds the given view holder with this item. - * - * @param adapter The adapter of this item. - * @param holder The holder to bind. - * @param position The position of this item in the adapter. - * @param payloads List of partial changes. - */ - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: CategoryHolder, - position: Int, - payloads: List?, - ) { - holder.bind(category) - } - - /** - * Returns true if this item is draggable. - */ - override fun isDraggable(): Boolean { - return true - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other is CategoryItem) { - return category.id == other.category.id - } - return false - } - - override fun hashCode(): Int { - return category.id.hashCode() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt index b509b5728..66926bb07 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt @@ -1,130 +1,91 @@ package eu.kanade.tachiyomi.ui.category -import android.os.Bundle +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import eu.kanade.domain.category.interactor.CreateCategoryWithName import eu.kanade.domain.category.interactor.DeleteCategory import eu.kanade.domain.category.interactor.GetCategories -import eu.kanade.domain.category.interactor.InsertCategory -import eu.kanade.domain.category.interactor.UpdateCategory +import eu.kanade.domain.category.interactor.RenameCategory +import eu.kanade.domain.category.interactor.ReorderCategory import eu.kanade.domain.category.model.Category -import eu.kanade.domain.category.model.CategoryUpdate -import eu.kanade.domain.category.repository.DuplicateNameException import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.lang.launchIO -import eu.kanade.tachiyomi.util.lang.launchUI -import eu.kanade.tachiyomi.util.system.logcat -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import logcat.LogPriority +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.consumeAsFlow import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -/** - * Presenter of [CategoryController]. Used to manage the categories of the library. - */ class CategoryPresenter( private val getCategories: GetCategories = Injekt.get(), - private val insertCategory: InsertCategory = Injekt.get(), - private val updateCategory: UpdateCategory = Injekt.get(), + private val createCategoryWithName: CreateCategoryWithName = Injekt.get(), + private val renameCategory: RenameCategory = Injekt.get(), + private val reorderCategory: ReorderCategory = Injekt.get(), private val deleteCategory: DeleteCategory = Injekt.get(), ) : BasePresenter() { - private val _categories: MutableStateFlow> = MutableStateFlow(listOf()) - val categories = _categories.asStateFlow() + var dialog: Dialog? by mutableStateOf(null) - /** - * Called when the presenter is created. - * - * @param savedState The saved state of this presenter. - */ - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) + val categories = getCategories.subscribe() - presenterScope.launchIO { - getCategories.subscribe() - .collectLatest { list -> - _categories.value = list - } - } - } + private val _events: Channel = Channel(Int.MAX_VALUE) + val events = _events.consumeAsFlow() - /** - * Creates and adds a new category to the database. - * - * @param name The name of the category to create. - */ fun createCategory(name: String) { presenterScope.launchIO { - val result = insertCategory.await( - name = name, - order = categories.value.map { it.order + 1L }.maxOrNull() ?: 0L, - ) - when (result) { - is InsertCategory.Result.Success -> {} - is InsertCategory.Result.Error -> { - logcat(LogPriority.ERROR, result.error) - if (result.error is DuplicateNameException) { - launchUI { view?.onCategoryExistsError() } - } - } + when (createCategoryWithName.await(name)) { + is CreateCategoryWithName.Result.NameAlreadyExistsError -> _events.send(Event.CategoryWithNameAlreadyExists) + is CreateCategoryWithName.Result.InternalError -> _events.send(Event.InternalError) + else -> {} } } } - /** - * Deletes the given categories from the database. - * - * @param categories The list of categories to delete. - */ - fun deleteCategories(categories: List) { + fun deleteCategory(category: Category) { presenterScope.launchIO { - categories.forEach { category -> - deleteCategory.await(category.id) + when (deleteCategory.await(category.id)) { + is DeleteCategory.Result.InternalError -> _events.send(Event.InternalError) + else -> {} } } } - /** - * Reorders the given categories in the database. - * - * @param categories The list of categories to reorder. - */ - fun reorderCategories(categories: List) { + fun moveUp(category: Category) { presenterScope.launchIO { - categories.forEachIndexed { order, category -> - updateCategory.await( - payload = CategoryUpdate( - id = category.id, - order = order.toLong(), - ), - ) + when (reorderCategory.await(category, category.order - 1)) { + is ReorderCategory.Result.InternalError -> _events.send(Event.InternalError) + else -> {} + } + } + } + + fun moveDown(category: Category) { + presenterScope.launchIO { + when (reorderCategory.await(category, category.order + 1)) { + is ReorderCategory.Result.InternalError -> _events.send(Event.InternalError) + else -> {} } } } - /** - * Renames a category. - * - * @param category The category to rename. - * @param name The new name of the category. - */ fun renameCategory(category: Category, name: String) { presenterScope.launchIO { - val result = updateCategory.await( - payload = CategoryUpdate( - id = category.id, - name = name, - ), - ) - when (result) { - is UpdateCategory.Result.Success -> {} - is UpdateCategory.Result.Error -> { - logcat(LogPriority.ERROR, result.error) - if (result.error is DuplicateNameException) { - launchUI { view?.onCategoryExistsError() } - } - } + when (renameCategory.await(category, name)) { + RenameCategory.Result.NameAlreadyExistsError -> _events.send(Event.CategoryWithNameAlreadyExists) + is RenameCategory.Result.InternalError -> _events.send(Event.InternalError) + else -> {} } } } + + sealed class Dialog { + object Create : Dialog() + data class Rename(val category: Category) : Dialog() + data class Delete(val category: Category) : Dialog() + } + + sealed class Event { + object CategoryWithNameAlreadyExists : Event() + object InternalError : Event() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt deleted file mode 100644 index a2946812b..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt +++ /dev/null @@ -1,83 +0,0 @@ -package eu.kanade.tachiyomi.ui.category - -import android.app.Dialog -import android.os.Bundle -import com.bluelinelabs.conductor.Controller -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.kanade.domain.category.model.Category -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput - -/** - * Dialog to rename an existing category of the library. - */ -class CategoryRenameDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T : CategoryRenameDialog.Listener { - - private var category: Category? = null - - /** - * Name of the new category. Value updated with each input from the user. - */ - private var currentName = "" - - constructor(target: T, category: Category) : this() { - targetController = target - this.category = category - currentName = category.name - } - - /** - * Called when creating the dialog for this controller. - * - * @param savedViewState The saved state of this dialog. - * @return a new dialog instance. - */ - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.action_rename_category) - .setTextInput(prefill = currentName) { - currentName = it - } - .setPositiveButton(android.R.string.ok) { _, _ -> onPositive() } - .setNegativeButton(android.R.string.cancel, null) - .create() - } - - /** - * Called to save this Controller's state in the event that its host Activity is destroyed. - * - * @param outState The Bundle into which data should be saved - */ - override fun onSaveInstanceState(outState: Bundle) { - outState.putSerializable(CATEGORY_KEY, category) - super.onSaveInstanceState(outState) - } - - /** - * Restores data that was saved in the [onSaveInstanceState] method. - * - * @param savedInstanceState The bundle that has data to be restored - */ - override fun onRestoreInstanceState(savedInstanceState: Bundle) { - super.onRestoreInstanceState(savedInstanceState) - category = savedInstanceState.getSerializable(CATEGORY_KEY) as? Category - } - - /** - * Called when the positive button of the dialog is clicked. - */ - private fun onPositive() { - val target = targetController as? Listener ?: return - val category = category ?: return - - target.renameCategory(category, currentName) - } - - interface Listener { - fun renameCategory(category: Category, name: String) - } -} - -private const val CATEGORY_KEY = "CategoryRenameDialog.category" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesController.kt index 0bfdc0143..34f494e4e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesController.kt @@ -25,12 +25,10 @@ import eu.kanade.tachiyomi.util.view.shrinkOnScroll import kotlin.time.Duration import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes -import kotlin.time.ExperimentalTime /** * Controller to manage the lock times for the biometric lock. */ -@OptIn(ExperimentalTime::class) class BiometricTimesController : NucleusController(), FabController, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesItem.kt index 7b9027931..b65c2c229 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesItem.kt @@ -6,12 +6,10 @@ import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R -import kotlin.time.ExperimentalTime /** * Category item for a recycler view. */ -@OptIn(ExperimentalTime::class) class BiometricTimesItem(val timeRange: TimeRange) : AbstractFlexibleItem() { /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesPresenter.kt index 6c80263d5..62197ab70 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesPresenter.kt @@ -3,20 +3,17 @@ package eu.kanade.tachiyomi.ui.category.biometric import android.os.Bundle import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.lang.launchUI +import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.preference.plusAssign -import exh.log.xLogD import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import rx.Observable -import rx.android.schedulers.AndroidSchedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import kotlin.time.ExperimentalTime /** * Presenter of [BiometricTimesController]. Used to manage the categories of the library. */ -@OptIn(ExperimentalTime::class) class BiometricTimesPresenter : BasePresenter() { /** @@ -36,12 +33,11 @@ class BiometricTimesPresenter : BasePresenter() { preferences.authenticatorTimeRanges().asFlow().onEach { prefTimeRanges -> timeRanges = prefTimeRanges.toList() - .mapNotNull { TimeRange.fromPreferenceString(it) }.onEach { xLogD(it) } + .mapNotNull(TimeRange::fromPreferenceString) - Observable.just(timeRanges) - .map { it.map(::BiometricTimesItem) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(BiometricTimesController::setBiometricTimeItems) + withUIContext { + view?.setBiometricTimeItems(timeRanges.map(::BiometricTimesItem)) + } }.launchIn(presenterScope) } @@ -53,12 +49,12 @@ class BiometricTimesPresenter : BasePresenter() { fun createTimeRange(timeRange: TimeRange) { // Do not allow duplicate categories. if (timeRangeConflicts(timeRange)) { - Observable.just(Unit).subscribeFirst({ view, _ -> view.onTimeRangeConflictsError() }) + launchUI { + view?.onTimeRangeConflictsError() + } return } - xLogD(timeRange) - preferences.authenticatorTimeRanges() += timeRange.toPreferenceString() } @@ -69,7 +65,7 @@ class BiometricTimesPresenter : BasePresenter() { */ fun deleteTimeRanges(timeRanges: List) { preferences.authenticatorTimeRanges().set( - this.timeRanges.filterNot { it in timeRanges }.map { it.toPreferenceString() }.toSet(), + this.timeRanges.filterNot { it in timeRanges }.map(TimeRange::toPreferenceString).toSet(), ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/TimeRange.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/TimeRange.kt index 7d4383c60..8eb4f4c52 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/TimeRange.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/TimeRange.kt @@ -40,11 +40,13 @@ data class TimeRange(private val startTime: Duration, private val endTime: Durat companion object { fun fromPreferenceString(timeRange: String): TimeRange? { - return timeRange.split(",").mapNotNull { it.toDoubleOrNull() }.let { - if (it.size != 2) null else { - TimeRange(it[0].minutes, it[1].minutes) - } - } + val index = timeRange.indexOf(',') + return if (index != -1) { + TimeRange( + timeRange.substring(0, index).toDoubleOrNull()?.minutes ?: return null, + timeRange.substring(index + 1).toDoubleOrNull()?.minutes ?: return null, + ) + } else return null } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagAdapter.kt deleted file mode 100644 index 921432270..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagAdapter.kt +++ /dev/null @@ -1,42 +0,0 @@ -package eu.kanade.tachiyomi.ui.category.genre - -import eu.davidea.flexibleadapter.FlexibleAdapter - -/** - * Custom adapter for categories. - * - * @param controller The containing controller. - */ -class SortTagAdapter(controller: SortTagController) : - FlexibleAdapter(null, controller, true) { - - /** - * Listener called when an item of the list is released. - */ - val onItemReleaseListener: OnItemReleaseListener = controller - - /** - * Clears the active selections from the list and the model. - */ - override fun clearSelection() { - super.clearSelection() - (0 until itemCount).forEach { getItem(it)?.isSelected = false } - } - - /** - * Toggles the selection of the given position. - * - * @param position The position to toggle. - */ - override fun toggleSelection(position: Int) { - super.toggleSelection(position) - getItem(position)?.isSelected = isSelected(position) - } - - interface OnItemReleaseListener { - /** - * Called when an item of the list is released. - */ - fun onItemReleased(position: Int) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagController.kt index 5ca8150f0..377f84f52 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagController.kt @@ -1,357 +1,21 @@ package eu.kanade.tachiyomi.ui.category.genre -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ActionMode -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton -import com.google.android.material.snackbar.Snackbar -import dev.chrisbanes.insetter.applyInsetter -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.davidea.flexibleadapter.helpers.UndoHelper -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.CategoriesControllerBinding -import eu.kanade.tachiyomi.ui.base.controller.FabController -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.shrinkOnScroll +import androidx.compose.runtime.Composable +import eu.kanade.presentation.category.SortTagScreen +import eu.kanade.tachiyomi.ui.base.controller.FullComposeController /** * Controller to manage the categories for the users' library. */ -class SortTagController : - NucleusController(), - FabController, - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - SortTagAdapter.OnItemReleaseListener, - SortTagCreateDialog.Listener, - UndoHelper.OnActionListener { +class SortTagController : FullComposeController() { - /** - * Object used to show ActionMode toolbar. - */ - private var actionMode: ActionMode? = null - - private var shownHelpDialog = false - - /** - * Adapter containing category items. - */ - private var adapter: SortTagAdapter? = null - - private var actionFab: ExtendedFloatingActionButton? = null - private var actionFabScrollListener: RecyclerView.OnScrollListener? = null - - /** - * Undo helper used for restoring a deleted category. - */ - private var undoHelper: UndoHelper? = null - - /** - * Creates the presenter for this controller. Not to be manually called. - */ override fun createPresenter() = SortTagPresenter() - /** - * Returns the toolbar title to show when this controller is attached. - */ - override fun getTitle(): String? { - return resources?.getString(R.string.action_edit_tags) - } - - init { - setHasOptionsMenu(true) - } - - override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater) - - /** - * Called after view inflation. Used to initialize the view. - * - * @param view The view of this controller. - */ - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - binding.recycler.applyInsetter { - type(navigationBars = true) { - padding() - } - } - - adapter = SortTagAdapter(this@SortTagController) - binding.recycler.layoutManager = LinearLayoutManager(view.context) - binding.recycler.setHasFixedSize(true) - binding.recycler.adapter = adapter - adapter?.isHandleDragEnabled = true - adapter?.isPermanentDelete = false - - actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler) - } - - override fun configureFab(fab: ExtendedFloatingActionButton) { - actionFab = fab - fab.setText(R.string.action_add) - fab.setIconResource(R.drawable.ic_add_24dp) - fab.setOnClickListener { - if (!shownHelpDialog) { - shownHelpDialog = true - helpDialog(true) - } else { - SortTagCreateDialog(this@SortTagController).showDialog(router, null) - } - } - } - - override fun cleanupFab(fab: ExtendedFloatingActionButton) { - fab.setOnClickListener(null) - actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) } - actionFab = null - } - - /** - * Called when the view is being destroyed. Used to release references and remove callbacks. - * - * @param view The view of this controller. - */ - override fun onDestroyView(view: View) { - // Manually call callback to delete categories if required - undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL) - undoHelper = null - actionMode = null - adapter = null - super.onDestroyView(view) - } - - /** - * Called from the presenter when the categories are updated. - * - * @param categories The new list of categories to display. - */ - fun setCategories(categories: List) { - actionMode?.finish() - adapter?.updateDataSet(categories) - if (categories.isNotEmpty()) { - binding.emptyView.hide() - val selected = categories.filter { it.isSelected } - if (selected.isNotEmpty()) { - selected.forEach { onItemLongClick(categories.indexOf(it)) } - } - } else { - binding.emptyView.show(R.string.information_empty_tags) - } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.sort_tags, menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_help -> { - shownHelpDialog = true - helpDialog() - } - else -> return false - } - return true - } - - /** - * Called when action mode is first created. The menu supplied will be used to generate action - * buttons for the action mode. - * - * @param mode ActionMode being created. - * @param menu Menu used to populate action buttons. - * @return true if the action mode should be created, false if entering this mode should be - * aborted. - */ - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - // Inflate menu. - mode.menuInflater.inflate(R.menu.category_selection, menu) - // Enable adapter multi selection. - adapter?.mode = SelectableAdapter.Mode.MULTI - return true - } - - /** - * Called to refresh an action mode's action menu whenever it is invalidated. - * - * @param mode ActionMode being prepared. - * @param menu Menu used to populate action buttons. - * @return true if the menu or action mode was updated, false otherwise. - */ - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val adapter = adapter ?: return false - val count = adapter.selectedItemCount - mode.title = count.toString() - - // Show edit button only when one item is selected - mode.menu.findItem(R.id.action_edit).isVisible = false - return true - } - - /** - * Called to report a user click on an action button. - * - * @param mode The current ActionMode. - * @param item The item that was clicked. - * @return true if this callback handled the event, false if the standard MenuItem invocation - * should continue. - */ - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - val adapter = adapter ?: return false - - when (item.itemId) { - R.id.action_delete -> { - undoHelper = UndoHelper(adapter, this) - undoHelper?.start( - adapter.selectedPositions, - (activity as? MainActivity)?.binding?.rootCoordinator!!, - R.string.snack_tags_deleted, - R.string.action_undo, - 3000, - ) - mode.finish() - } - else -> return false - } - return true - } - - private fun helpDialog(hasPositive: Boolean = false) { - MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.add_tag) - .setMessage(R.string.action_add_tags_message) - .setPositiveButton(android.R.string.ok) { _, _ -> - if (hasPositive) { - SortTagCreateDialog(this@SortTagController).showDialog(router, null) - } - } - .show() - } - - /** - * Called when an action mode is about to be exited and destroyed. - * - * @param mode The current ActionMode being destroyed. - */ - override fun onDestroyActionMode(mode: ActionMode) { - // Reset adapter to single selection - adapter?.mode = SelectableAdapter.Mode.IDLE - adapter?.clearSelection() - actionMode = null - } - - /** - * Called when an item in the list is clicked. - * - * @param position The position of the clicked item. - * @return true if this click should enable selection mode. - */ - override fun onItemClick(view: View, position: Int): Boolean { - // Check if action mode is initialized and selected item exist. - return if (actionMode != null && position != RecyclerView.NO_POSITION) { - toggleSelection(position) - true - } else { - false - } - } - - /** - * Called when an item in the list is long clicked. - * - * @param position The position of the clicked item. - */ - override fun onItemLongClick(position: Int) { - val activity = activity as? AppCompatActivity ?: return - - // Check if action mode is initialized. - if (actionMode == null) { - // Initialize action mode - actionMode = activity.startSupportActionMode(this) - } - - // Set item as selected - toggleSelection(position) - } - - /** - * Toggle the selection state of an item. - * If the item was the last one in the selection and is unselected, the ActionMode is finished. - * - * @param position The position of the item to toggle. - */ - private fun toggleSelection(position: Int) { - val adapter = adapter ?: return - - // Mark the position selected - adapter.toggleSelection(position) - - if (adapter.selectedItemCount == 0) { - actionMode?.finish() - } else { - actionMode?.invalidate() - } - } - - /** - * Called when an item is released from a drag. - * - * @param position The position of the released item. - */ - override fun onItemReleased(position: Int) { - val adapter = adapter ?: return - val tags = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.tag } - presenter.reorderTags(tags) - } - - /** - * Called when the undo action is clicked in the snackbar. - * - * @param action The action performed. - */ - override fun onActionCanceled(action: Int, positions: MutableList?) { - adapter?.restoreDeletedItems() - undoHelper = null - } - - /** - * Called when the time to restore the items expires. - * - * @param action The action performed. - * @param event The event that triggered the action - */ - override fun onActionConfirmed(action: Int, event: Int) { - val adapter = adapter ?: return - presenter.deleteTags(adapter.deletedItems.map { it.tag }) - undoHelper = null - } - - /** - * Creates a new category with the given name. - * - * @param name The name of the new category. - */ - override fun createCategory(name: String) { - presenter.createTag(name) - } - - /** - * Called from the presenter when a category with the given name already exists. - */ - fun onTagExistsError() { - activity?.toast(R.string.error_tag_exists) + @Composable + override fun ComposeContent() { + SortTagScreen( + presenter = presenter, + navigateUp = router::popCurrentController, + ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagCreateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagCreateDialog.kt deleted file mode 100644 index 1fd3fc74f..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagCreateDialog.kt +++ /dev/null @@ -1,51 +0,0 @@ -package eu.kanade.tachiyomi.ui.category.genre - -import android.app.Dialog -import android.os.Bundle -import com.bluelinelabs.conductor.Controller -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput - -/** - * Dialog to create a new category for the library. - */ -class SortTagCreateDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T : SortTagCreateDialog.Listener { - - /** - * Name of the new category. Value updated with each input from the user. - */ - private var currentName = "" - - constructor(target: T) : this() { - targetController = target - } - - /** - * Called when creating the dialog for this controller. - * - * @param savedViewState The saved state of this dialog. - * @return a new dialog instance. - */ - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.action_add_category) - .setTextInput( - hint = resources?.getString(R.string.name), - prefill = currentName, - ) { input -> - currentName = input - } - .setPositiveButton(android.R.string.ok) { _, _ -> - (targetController as? Listener)?.createCategory(currentName) - } - .setNegativeButton(android.R.string.cancel, null) - .create() - } - - interface Listener { - fun createCategory(name: String) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagHolder.kt deleted file mode 100644 index bb7ce8532..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagHolder.kt +++ /dev/null @@ -1,39 +0,0 @@ -package eu.kanade.tachiyomi.ui.category.genre - -import android.view.View -import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.tachiyomi.databinding.CategoriesItemBinding - -/** - * Holder used to display category items. - * - * @param view The view used by category items. - * @param adapter The adapter containing this holder. - */ -class SortTagHolder(view: View, val adapter: SortTagAdapter) : FlexibleViewHolder(view, adapter) { - - private val binding = CategoriesItemBinding.bind(view) - - init { - setDragHandleView(binding.reorder) - } - - /** - * Binds this holder with the given category. - * - * @param tag The tag to bind. - */ - fun bind(tag: String) { - binding.title.text = tag - } - - /** - * Called when an item is released. - * - * @param position The position of the released item. - */ - override fun onItemReleased(position: Int) { - super.onItemReleased(position) - adapter.onItemReleaseListener.onItemReleased(position) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagItem.kt deleted file mode 100644 index 47a08b600..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagItem.kt +++ /dev/null @@ -1,72 +0,0 @@ -package eu.kanade.tachiyomi.ui.category.genre - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R - -/** - * Category item for a recycler view. - */ -class SortTagItem(val tag: String) : AbstractFlexibleItem() { - - /** - * Whether this item is currently selected. - */ - var isSelected = false - - /** - * Returns the layout resource for this item. - */ - override fun getLayoutRes(): Int { - return R.layout.categories_item - } - - /** - * Returns a new view holder for this item. - * - * @param view The view of this item. - * @param adapter The adapter of this item. - */ - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): SortTagHolder { - return SortTagHolder(view, adapter as SortTagAdapter) - } - - /** - * Binds the given view holder with this item. - * - * @param adapter The adapter of this item. - * @param holder The holder to bind. - * @param position The position of this item in the adapter. - * @param payloads List of partial changes. - */ - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: SortTagHolder, - position: Int, - payloads: List?, - ) { - holder.bind(tag) - } - - /** - * Returns true if this item is draggable. - */ - override fun isDraggable(): Boolean { - return true - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other is SortTagItem) { - return tag == other.tag - } - return false - } - - override fun hashCode(): Int { - return tag.hashCode() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagPresenter.kt index cf05b21ab..c91928f7f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/genre/SortTagPresenter.kt @@ -1,93 +1,79 @@ package eu.kanade.tachiyomi.ui.category.genre -import android.os.Bundle -import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import eu.kanade.domain.manga.interactor.CreateSortTag +import eu.kanade.domain.manga.interactor.DeleteSortTag +import eu.kanade.domain.manga.interactor.GetSortTag +import eu.kanade.domain.manga.interactor.ReorderSortTag import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.preference.minusAssign -import eu.kanade.tachiyomi.util.preference.plusAssign -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import rx.Observable -import rx.android.schedulers.AndroidSchedulers +import eu.kanade.tachiyomi.util.lang.launchIO +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.consumeAsFlow import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get /** * Presenter of [SortTagController]. Used to manage the categories of the library. */ -class SortTagPresenter : BasePresenter() { +class SortTagPresenter( + private val getSortTag: GetSortTag = Injekt.get(), + private val createSortTag: CreateSortTag = Injekt.get(), + private val deleteSortTag: DeleteSortTag = Injekt.get(), + private val reorderSortTag: ReorderSortTag = Injekt.get(), +) : BasePresenter() { + + var dialog: Dialog? by mutableStateOf(null) /** * List containing categories. */ - private var tags: List> = emptyList() + val tags = getSortTag.subscribe() - val preferences: PreferencesHelper = Injekt.get() + private val _events: Channel = Channel(Int.MAX_VALUE) + val events = _events.consumeAsFlow() - /** - * Called when the presenter is created. - * - * @param savedState The saved state of this presenter. - */ - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - preferences.sortTagsForLibrary().asFlow().onEach { tags -> - this.tags = tags.map { it.split("|") } - .mapNotNull { (it.getOrNull(0)?.toIntOrNull() ?: return@mapNotNull null) to (it.getOrNull(1) ?: return@mapNotNull null) } - .sortedBy { it.first } - - Observable.just(this.tags) - .map { tagPairs -> tagPairs.map { it.second }.map(::SortTagItem) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(SortTagController::setCategories) - }.launchIn(presenterScope) - } - - /** - * Creates and adds a new category to the database. - * - * @param name The name of the category to create. - */ fun createTag(name: String) { - // Do not allow duplicate categories. - if (tagExists(name.trim())) { - Observable.just(Unit).subscribeFirst({ view, _ -> view.onTagExistsError() }) - return - } - - val size = preferences.sortTagsForLibrary().get().size - - preferences.sortTagsForLibrary() += "$size|${name.trim()}" - } - - /** - * Deletes the given categories from the database. - * - * @param tags The list of categories to delete. - */ - fun deleteTags(tags: List) { - val preferenceTags = preferences.sortTagsForLibrary().get() - tags.forEach { tag -> - preferenceTags.firstOrNull { it.endsWith(tag) }?.let { - preferences.sortTagsForLibrary() -= it + presenterScope.launchIO { + when (createSortTag.await(name)) { + is CreateSortTag.Result.TagExists -> _events.send(Event.TagExists) + else -> {} } } } - /** - * Reorders the given categories in the database. - * - * @param tags The list of categories to reorder. - */ - fun reorderTags(tags: List) { - preferences.sortTagsForLibrary().set(tags.mapIndexed { index, tag -> "$index|$tag" }.toSet()) + fun delete(tag: String) { + presenterScope.launchIO { + deleteSortTag.await(tag) + } } - /** - * Returns true if a category with the given name already exists. - */ - private fun tagExists(name: String): Boolean { - return tags.any { it.equals(name) } + fun moveUp(tag: String, index: Int) { + presenterScope.launchIO { + when (reorderSortTag.await(tag, index - 1)) { + is ReorderSortTag.Result.InternalError -> _events.send(Event.InternalError) + else -> {} + } + } + } + + fun moveDown(tag: String, index: Int) { + presenterScope.launchIO { + when (reorderSortTag.await(tag, index + 1)) { + is ReorderSortTag.Result.InternalError -> _events.send(Event.InternalError) + else -> {} + } + } + } + + sealed class Event { + object TagExists : Event() + object InternalError : Event() + } + + sealed class Dialog { + object Create : Dialog() + data class Delete(val tag: String) : Dialog() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoAdapter.kt deleted file mode 100644 index b90302176..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoAdapter.kt +++ /dev/null @@ -1,30 +0,0 @@ -package eu.kanade.tachiyomi.ui.category.repos - -import eu.davidea.flexibleadapter.FlexibleAdapter - -/** - * Custom adapter for repos. - * - * @param controller The containing controller. - */ -class RepoAdapter(controller: RepoController) : - FlexibleAdapter(null, controller, true) { - - /** - * Clears the active selections from the list and the model. - */ - override fun clearSelection() { - super.clearSelection() - (0 until itemCount).forEach { getItem(it)?.isSelected = false } - } - - /** - * Toggles the selection of the given position. - * - * @param position The position to toggle. - */ - override fun toggleSelection(position: Int) { - super.toggleSelection(position) - getItem(position)?.isSelected = isSelected(position) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoController.kt index 9bc167ace..0e91fccbf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoController.kt @@ -1,313 +1,21 @@ package eu.kanade.tachiyomi.ui.category.repos -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuItem -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ActionMode -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton -import com.google.android.material.snackbar.Snackbar -import dev.chrisbanes.insetter.applyInsetter -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.davidea.flexibleadapter.helpers.UndoHelper -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.CategoriesControllerBinding -import eu.kanade.tachiyomi.ui.base.controller.FabController -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.shrinkOnScroll +import androidx.compose.runtime.Composable +import eu.kanade.presentation.category.SourceRepoScreen +import eu.kanade.tachiyomi.ui.base.controller.FullComposeController /** * Controller to manage the categories for the users' library. */ -class RepoController : - NucleusController(), - FabController, - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - RepoCreateDialog.Listener, - UndoHelper.OnActionListener { +class RepoController : FullComposeController() { - /** - * Object used to show ActionMode toolbar. - */ - private var actionMode: ActionMode? = null - - /** - * Adapter containing repo items. - */ - private var adapter: RepoAdapter? = null - - private var actionFab: ExtendedFloatingActionButton? = null - private var actionFabScrollListener: RecyclerView.OnScrollListener? = null - - /** - * Undo helper used for restoring a deleted repo. - */ - private var undoHelper: UndoHelper? = null - - /** - * Creates the presenter for this controller. Not to be manually called. - */ override fun createPresenter() = RepoPresenter() - /** - * Returns the toolbar title to show when this controller is attached. - */ - override fun getTitle(): String? { - return resources?.getString(R.string.action_edit_repos) - } - - override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater) - - /** - * Called after view inflation. Used to initialize the view. - * - * @param view The view of this controller. - */ - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - binding.recycler.applyInsetter { - type(navigationBars = true) { - padding() - } - } - - adapter = RepoAdapter(this@RepoController) - binding.recycler.layoutManager = LinearLayoutManager(view.context) - binding.recycler.setHasFixedSize(true) - binding.recycler.adapter = adapter - adapter?.isPermanentDelete = false - - actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler) - } - - override fun configureFab(fab: ExtendedFloatingActionButton) { - actionFab = fab - fab.setText(R.string.action_add) - fab.setIconResource(R.drawable.ic_add_24dp) - fab.setOnClickListener { - RepoCreateDialog(this@RepoController).showDialog(router, null) - } - } - - override fun cleanupFab(fab: ExtendedFloatingActionButton) { - fab.setOnClickListener(null) - actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) } - actionFab = null - } - - /** - * Called when the view is being destroyed. Used to release references and remove callbacks. - * - * @param view The view of this controller. - */ - override fun onDestroyView(view: View) { - // Manually call callback to delete repos if required - undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL) - undoHelper = null - actionMode = null - adapter = null - super.onDestroyView(view) - } - - /** - * Called from the presenter when the repos are updated. - * - * @param repos The new list of repos to display. - */ - fun setRepos(repos: List) { - actionMode?.finish() - adapter?.updateDataSet(repos) - if (repos.isNotEmpty()) { - binding.emptyView.hide() - val selected = repos.filter { it.isSelected } - if (selected.isNotEmpty()) { - selected.forEach { onItemLongClick(repos.indexOf(it)) } - } - } else { - binding.emptyView.show(R.string.information_empty_repos) - } - } - - /** - * Called when action mode is first created. The menu supplied will be used to generate action - * buttons for the action mode. - * - * @param mode ActionMode being created. - * @param menu Menu used to populate action buttons. - * @return true if the action mode should be created, false if entering this mode should be - * aborted. - */ - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - // Inflate menu. - mode.menuInflater.inflate(R.menu.category_selection, menu) - // Enable adapter multi selection. - adapter?.mode = SelectableAdapter.Mode.MULTI - return true - } - - /** - * Called to refresh an action mode's action menu whenever it is invalidated. - * - * @param mode ActionMode being prepared. - * @param menu Menu used to populate action buttons. - * @return true if the menu or action mode was updated, false otherwise. - */ - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val adapter = adapter ?: return false - val count = adapter.selectedItemCount - mode.title = count.toString() - - // Show edit button only when one item is selected - val editItem = mode.menu.findItem(R.id.action_edit) - editItem.isVisible = false - return true - } - - /** - * Called to report a user click on an action button. - * - * @param mode The current ActionMode. - * @param item The item that was clicked. - * @return true if this callback handled the event, false if the standard MenuItem invocation - * should continue. - */ - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - val adapter = adapter ?: return false - - when (item.itemId) { - R.id.action_delete -> { - undoHelper = UndoHelper(adapter, this) - undoHelper?.start( - adapter.selectedPositions, - (activity as? MainActivity)?.binding?.rootCoordinator!!, - R.string.snack_repo_deleted, - R.string.action_undo, - 3000, - ) - - mode.finish() - } - else -> return false - } - return true - } - - /** - * Called when an action mode is about to be exited and destroyed. - * - * @param mode The current ActionMode being destroyed. - */ - override fun onDestroyActionMode(mode: ActionMode) { - // Reset adapter to single selection - adapter?.mode = SelectableAdapter.Mode.IDLE - adapter?.clearSelection() - actionMode = null - } - - /** - * Called when an item in the list is clicked. - * - * @param position The position of the clicked item. - * @return true if this click should enable selection mode. - */ - override fun onItemClick(view: View, position: Int): Boolean { - // Check if action mode is initialized and selected item exist. - return if (actionMode != null && position != RecyclerView.NO_POSITION) { - toggleSelection(position) - true - } else { - false - } - } - - /** - * Called when an item in the list is long clicked. - * - * @param position The position of the clicked item. - */ - override fun onItemLongClick(position: Int) { - val activity = activity as? AppCompatActivity ?: return - - // Check if action mode is initialized. - if (actionMode == null) { - // Initialize action mode - actionMode = activity.startSupportActionMode(this) - } - - // Set item as selected - toggleSelection(position) - } - - /** - * Toggle the selection state of an item. - * If the item was the last one in the selection and is unselected, the ActionMode is finished. - * - * @param position The position of the item to toggle. - */ - private fun toggleSelection(position: Int) { - val adapter = adapter ?: return - - // Mark the position selected - adapter.toggleSelection(position) - - if (adapter.selectedItemCount == 0) { - actionMode?.finish() - } else { - actionMode?.invalidate() - } - } - - /** - * Called when the undo action is clicked in the snackbar. - * - * @param action The action performed. - */ - override fun onActionCanceled(action: Int, positions: MutableList?) { - adapter?.restoreDeletedItems() - undoHelper = null - } - - /** - * Called when the time to restore the items expires. - * - * @param action The action performed. - * @param event The event that triggered the action - */ - override fun onActionConfirmed(action: Int, event: Int) { - val adapter = adapter ?: return - presenter.deleteRepos(adapter.deletedItems.map { it.repo }) - undoHelper = null - } - - /** - * Creates a new repo with the given name. - * - * @param name The name of the new repo. - */ - override fun createRepo(name: String) { - presenter.createRepo(name) - } - - /** - * Called from the presenter when a repo already exists. - */ - fun onRepoExistsError() { - activity?.toast(R.string.error_repo_exists) - } - - /** - * Called from the presenter when a invalid repo is made - */ - fun onRepoInvalidNameError() { - activity?.toast(R.string.invalid_repo_name) + @Composable + override fun ComposeContent() { + SourceRepoScreen( + presenter = presenter, + navigateUp = router::popCurrentController, + ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoCreateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoCreateDialog.kt deleted file mode 100644 index 2a163ca71..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoCreateDialog.kt +++ /dev/null @@ -1,52 +0,0 @@ -package eu.kanade.tachiyomi.ui.category.repos - -import android.app.Dialog -import android.os.Bundle -import com.bluelinelabs.conductor.Controller -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput - -/** - * Dialog to create a new repo for the library. - */ -class RepoCreateDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T : RepoCreateDialog.Listener { - - /** - * Name of the new repo. Value updated with each input from the user. - */ - private var currentName = "" - - constructor(target: T) : this() { - targetController = target - } - - /** - * Called when creating the dialog for this controller. - * - * @param savedViewState The saved state of this dialog. - * @return a new dialog instance. - */ - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.action_add_repo) - .setMessage(R.string.action_add_repo_message) - .setTextInput( - hint = resources?.getString(R.string.name), - prefill = currentName, - ) { input -> - currentName = input - } - .setPositiveButton(android.R.string.ok) { _, _ -> - (targetController as? Listener)?.createRepo(currentName) - } - .setNegativeButton(android.R.string.cancel, null) - .create() - } - - interface Listener { - fun createRepo(name: String) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoHolder.kt deleted file mode 100644 index bcdcd55c6..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoHolder.kt +++ /dev/null @@ -1,31 +0,0 @@ -package eu.kanade.tachiyomi.ui.category.repos - -import android.view.View -import androidx.core.view.isVisible -import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.tachiyomi.databinding.CategoriesItemBinding -import eu.kanade.tachiyomi.util.system.dpToPx - -/** - * Holder used to display repo items. - * - * @param view The view used by repo items. - * @param adapter The adapter containing this holder. - */ -class RepoHolder(view: View, val adapter: RepoAdapter) : FlexibleViewHolder(view, adapter) { - - private val binding = CategoriesItemBinding.bind(view) - - /** - * Binds this holder with the given category. - * - * @param category The category to bind. - */ - fun bind(category: String) { - binding.innerContainer.minimumHeight = 60.dpToPx - - // Set capitalized title. - binding.title.text = category - binding.reorder.isVisible = false - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoItem.kt deleted file mode 100644 index 9ccbfde1f..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoItem.kt +++ /dev/null @@ -1,62 +0,0 @@ -package eu.kanade.tachiyomi.ui.category.repos - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R - -/** - * Repo item for a recycler view. - */ -class RepoItem(val repo: String) : AbstractFlexibleItem() { - - /** - * Whether this item is currently selected. - */ - var isSelected = false - - /** - * Returns the layout resource for this item. - */ - override fun getLayoutRes(): Int { - return R.layout.categories_item - } - - /** - * Returns a new view holder for this item. - * - * @param view The view of this item. - * @param adapter The adapter of this item. - */ - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): RepoHolder { - return RepoHolder(view, adapter as RepoAdapter) - } - - /** - * Binds the given view holder with this item. - * - * @param adapter The adapter of this item. - * @param holder The holder to bind. - * @param position The position of this item in the adapter. - * @param payloads List of partial changes. - */ - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: RepoHolder, - position: Int, - payloads: List?, - ) { - holder.bind(repo) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - return false - } - - override fun hashCode(): Int { - return repo.hashCode() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoPresenter.kt index ef671a538..e03a23d13 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/repos/RepoPresenter.kt @@ -1,12 +1,15 @@ package eu.kanade.tachiyomi.ui.category.repos -import android.os.Bundle -import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import eu.kanade.domain.source.interactor.CreateSourceRepo +import eu.kanade.domain.source.interactor.DeleteSourceRepos +import eu.kanade.domain.source.interactor.GetSourceRepos import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import rx.Observable -import rx.android.schedulers.AndroidSchedulers +import eu.kanade.tachiyomi.util.lang.launchIO +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.consumeAsFlow import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -14,30 +17,17 @@ import uy.kohesive.injekt.api.get * Presenter of [RepoController]. Used to manage the repos for the extensions. */ class RepoPresenter( - private val preferences: PreferencesHelper = Injekt.get(), + private val getSourceRepos: GetSourceRepos = Injekt.get(), + private val createSourceRepo: CreateSourceRepo = Injekt.get(), + private val deleteSourceRepos: DeleteSourceRepos = Injekt.get(), ) : BasePresenter() { - /** - * List containing repos. - */ - private var repos: List = emptyList() - /** - * Called when the presenter is created. - * - * @param savedState The saved state of this presenter. - */ - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) + var dialog: Dialog? by mutableStateOf(null) - preferences.extensionRepos().asFlow().onEach { repos -> - this.repos = repos.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it })) + val repos = getSourceRepos.subscribe() - Observable.just(this.repos) - .map { it.map(::RepoItem) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(RepoController::setRepos) - }.launchIn(presenterScope) - } + private val _events: Channel = Channel(Int.MAX_VALUE) + val events = _events.consumeAsFlow() /** * Creates and adds a new repo to the database. @@ -45,19 +35,13 @@ class RepoPresenter( * @param name The name of the repo to create. */ fun createRepo(name: String) { - // Do not allow duplicate repos. - if (repoExists(name)) { - Observable.just(Unit).subscribeFirst({ view, _ -> view.onRepoExistsError() }) - return + presenterScope.launchIO { + when (createSourceRepo.await(name)) { + is CreateSourceRepo.Result.RepoExists -> _events.send(Event.RepoExists) + is CreateSourceRepo.Result.InvalidName -> _events.send(Event.InvalidName) + else -> {} + } } - - // Do not allow invalid formats - if (!name.matches(repoRegex)) { - Observable.just(Unit).subscribeFirst({ view, _ -> view.onRepoInvalidNameError() }) - return - } - - preferences.extensionRepos().set((repos + name).toSet()) } /** @@ -66,19 +50,19 @@ class RepoPresenter( * @param repos The list of repos to delete. */ fun deleteRepos(repos: List) { - preferences.extensionRepos().set( - this.repos.filterNot { it in repos }.toSet(), - ) + presenterScope.launchIO { + deleteSourceRepos.await(repos) + } } - /** - * Returns true if a repo with the given name already exists. - */ - private fun repoExists(name: String): Boolean { - return repos.any { it.equals(name, true) } + sealed class Event { + object RepoExists : Event() + object InvalidName : Event() + object InternalError : Event() } - companion object { - val repoRegex = """^[a-zA-Z0-9-_.]*?\/[a-zA-Z0-9-_.]*?$""".toRegex() + sealed class Dialog { + object Create : Dialog() + data class Delete(val repo: String) : Dialog() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/ChangeSourceCategoriesDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/ChangeSourceCategoriesDialog.kt deleted file mode 100644 index 70f98d89a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/ChangeSourceCategoriesDialog.kt +++ /dev/null @@ -1,54 +0,0 @@ -package eu.kanade.tachiyomi.ui.category.sources - -import android.app.Dialog -import android.os.Bundle -import com.bluelinelabs.conductor.Controller -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.ui.base.controller.DialogController - -class ChangeSourceCategoriesDialog(bundle: Bundle? = null) : - DialogController(bundle) where T : Controller, T : ChangeSourceCategoriesDialog.Listener { - - private var source: Source? = null - - private var categories = emptyArray() - - private var selection = booleanArrayOf() - - constructor( - target: T, - source: Source, - categories: Array, - selection: BooleanArray, - ) : this() { - this.source = source - this.categories = categories - this.selection = selection - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.action_move_category) - .setMultiChoiceItems( - categories, - selection, - ) { _, which, selected -> - selection[which] = selected - } - .setPositiveButton(android.R.string.ok) { _, _ -> - val newCategories = categories.filterIndexed { index, s -> - selection[index] - } - (targetController as? Listener)?.updateCategoriesForSource(source!!, newCategories) - } - .setNegativeButton(android.R.string.cancel, null) - .create() - } - - interface Listener { - fun updateCategoriesForSource(source: Source, categories: List) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryAdapter.kt deleted file mode 100644 index 8be27e407..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryAdapter.kt +++ /dev/null @@ -1,30 +0,0 @@ -package eu.kanade.tachiyomi.ui.category.sources - -import eu.davidea.flexibleadapter.FlexibleAdapter - -/** - * Custom adapter for categories. - * - * @param controller The containing controller. - */ -class SourceCategoryAdapter(controller: SourceCategoryController) : - FlexibleAdapter(null, controller, true) { - - /** - * Clears the active selections from the list and the model. - */ - override fun clearSelection() { - super.clearSelection() - (0 until itemCount).forEach { getItem(it)?.isSelected = false } - } - - /** - * Toggles the selection of the given position. - * - * @param position The position to toggle. - */ - override fun toggleSelection(position: Int) { - super.toggleSelection(position) - getItem(position)?.isSelected = isSelected(position) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryController.kt index cc872b6b6..bb9222422 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryController.kt @@ -1,343 +1,21 @@ package eu.kanade.tachiyomi.ui.category.sources -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuItem -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ActionMode -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton -import com.google.android.material.snackbar.Snackbar -import dev.chrisbanes.insetter.applyInsetter -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.davidea.flexibleadapter.helpers.UndoHelper -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.CategoriesControllerBinding -import eu.kanade.tachiyomi.ui.base.controller.FabController -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.shrinkOnScroll +import androidx.compose.runtime.Composable +import eu.kanade.presentation.category.SourceCategoryScreen +import eu.kanade.tachiyomi.ui.base.controller.FullComposeController /** * Controller to manage the categories for the users' library. */ -class SourceCategoryController : - NucleusController(), - FabController, - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - SourceCategoryCreateDialog.Listener, - SourceCategoryRenameDialog.Listener, - UndoHelper.OnActionListener { +class SourceCategoryController : FullComposeController() { - /** - * Object used to show ActionMode toolbar. - */ - private var actionMode: ActionMode? = null - - /** - * Adapter containing category items. - */ - private var adapter: SourceCategoryAdapter? = null - - private var actionFab: ExtendedFloatingActionButton? = null - private var actionFabScrollListener: RecyclerView.OnScrollListener? = null - - /** - * Undo helper used for restoring a deleted category. - */ - private var undoHelper: UndoHelper? = null - - /** - * Creates the presenter for this controller. Not to be manually called. - */ override fun createPresenter() = SourceCategoryPresenter() - /** - * Returns the toolbar title to show when this controller is attached. - */ - override fun getTitle(): String? { - return resources?.getString(R.string.action_edit_categories) - } - - override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater) - - /** - * Called after view inflation. Used to initialize the view. - * - * @param view The view of this controller. - */ - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - binding.recycler.applyInsetter { - type(navigationBars = true) { - padding() - } - } - - adapter = SourceCategoryAdapter(this@SourceCategoryController) - binding.recycler.layoutManager = LinearLayoutManager(view.context) - binding.recycler.setHasFixedSize(true) - binding.recycler.adapter = adapter - adapter?.isPermanentDelete = false - - actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler) - } - - override fun configureFab(fab: ExtendedFloatingActionButton) { - actionFab = fab - fab.setText(R.string.action_add) - fab.setIconResource(R.drawable.ic_add_24dp) - fab.setOnClickListener { - SourceCategoryCreateDialog(this@SourceCategoryController).showDialog(router, null) - } - } - - override fun cleanupFab(fab: ExtendedFloatingActionButton) { - fab.setOnClickListener(null) - actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) } - actionFab = null - } - - /** - * Called when the view is being destroyed. Used to release references and remove callbacks. - * - * @param view The view of this controller. - */ - override fun onDestroyView(view: View) { - // Manually call callback to delete categories if required - undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL) - undoHelper = null - actionMode = null - adapter = null - super.onDestroyView(view) - } - - /** - * Called from the presenter when the categories are updated. - * - * @param categories The new list of categories to display. - */ - fun setCategories(categories: List) { - actionMode?.finish() - adapter?.updateDataSet(categories) - if (categories.isNotEmpty()) { - binding.emptyView.hide() - val selected = categories.filter { it.isSelected } - if (selected.isNotEmpty()) { - selected.forEach { onItemLongClick(categories.indexOf(it)) } - } - } else { - binding.emptyView.show(R.string.information_empty_category) - } - } - - /** - * Called when action mode is first created. The menu supplied will be used to generate action - * buttons for the action mode. - * - * @param mode ActionMode being created. - * @param menu Menu used to populate action buttons. - * @return true if the action mode should be created, false if entering this mode should be - * aborted. - */ - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - // Inflate menu. - mode.menuInflater.inflate(R.menu.category_selection, menu) - // Enable adapter multi selection. - adapter?.mode = SelectableAdapter.Mode.MULTI - return true - } - - /** - * Called to refresh an action mode's action menu whenever it is invalidated. - * - * @param mode ActionMode being prepared. - * @param menu Menu used to populate action buttons. - * @return true if the menu or action mode was updated, false otherwise. - */ - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val adapter = adapter ?: return false - val count = adapter.selectedItemCount - mode.title = count.toString() - - // Show edit button only when one item is selected - val editItem = mode.menu.findItem(R.id.action_edit) - editItem.isVisible = count == 1 - return true - } - - /** - * Called to report a user click on an action button. - * - * @param mode The current ActionMode. - * @param item The item that was clicked. - * @return true if this callback handled the event, false if the standard MenuItem invocation - * should continue. - */ - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - val adapter = adapter ?: return false - - when (item.itemId) { - R.id.action_delete -> { - undoHelper = UndoHelper(adapter, this) - undoHelper?.start( - adapter.selectedPositions, - (activity as? MainActivity)?.binding?.rootCoordinator!!, - R.string.snack_categories_deleted, - R.string.action_undo, - 3000, - ) - - mode.finish() - } - R.id.action_edit -> { - // Edit selected category - if (adapter.selectedItemCount == 1) { - val position = adapter.selectedPositions.first() - val category = adapter.getItem(position)?.category - if (category != null) { - editCategory(category) - } - } - } - else -> return false - } - return true - } - - /** - * Called when an action mode is about to be exited and destroyed. - * - * @param mode The current ActionMode being destroyed. - */ - override fun onDestroyActionMode(mode: ActionMode) { - // Reset adapter to single selection - adapter?.mode = SelectableAdapter.Mode.IDLE - adapter?.clearSelection() - actionMode = null - } - - /** - * Called when an item in the list is clicked. - * - * @param position The position of the clicked item. - * @return true if this click should enable selection mode. - */ - override fun onItemClick(view: View, position: Int): Boolean { - // Check if action mode is initialized and selected item exist. - return if (actionMode != null && position != RecyclerView.NO_POSITION) { - toggleSelection(position) - true - } else { - false - } - } - - /** - * Called when an item in the list is long clicked. - * - * @param position The position of the clicked item. - */ - override fun onItemLongClick(position: Int) { - val activity = activity as? AppCompatActivity ?: return - - // Check if action mode is initialized. - if (actionMode == null) { - // Initialize action mode - actionMode = activity.startSupportActionMode(this) - } - - // Set item as selected - toggleSelection(position) - } - - /** - * Toggle the selection state of an item. - * If the item was the last one in the selection and is unselected, the ActionMode is finished. - * - * @param position The position of the item to toggle. - */ - private fun toggleSelection(position: Int) { - val adapter = adapter ?: return - - // Mark the position selected - adapter.toggleSelection(position) - - if (adapter.selectedItemCount == 0) { - actionMode?.finish() - } else { - actionMode?.invalidate() - } - } - - /** - * Called when the undo action is clicked in the snackbar. - * - * @param action The action performed. - */ - override fun onActionCanceled(action: Int, positions: MutableList?) { - adapter?.restoreDeletedItems() - undoHelper = null - } - - /** - * Called when the time to restore the items expires. - * - * @param action The action performed. - * @param event The event that triggered the action - */ - override fun onActionConfirmed(action: Int, event: Int) { - val adapter = adapter ?: return - presenter.deleteCategories(adapter.deletedItems.map { it.category }) - undoHelper = null - } - - /** - * Show a dialog to let the user change the category name. - * - * @param category The category to be edited. - */ - private fun editCategory(category: String) { - SourceCategoryRenameDialog(this, category).showDialog(router) - } - - /** - * Renames the given category with the given name. - * - * @param category The category to rename. - * @param name The new name of the category. - */ - override fun renameCategory(category: String, name: String) { - presenter.renameCategory(category, name) - } - - /** - * Creates a new category with the given name. - * - * @param name The name of the new category. - */ - override fun createCategory(name: String) { - presenter.createCategory(name) - } - - /** - * Called from the presenter when a category with the given name already exists. - */ - fun onCategoryExistsError() { - activity?.toast(R.string.error_category_exists) - } - - /** - * Called from the presenter when a invalid category name is made - */ - fun onCategoryInvalidNameError() { - activity?.toast(R.string.invalid_category_name) + @Composable + override fun ComposeContent() { + SourceCategoryScreen( + presenter = presenter, + navigateUp = router::popCurrentController, + ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryCreateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryCreateDialog.kt deleted file mode 100644 index f3945b9e2..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryCreateDialog.kt +++ /dev/null @@ -1,51 +0,0 @@ -package eu.kanade.tachiyomi.ui.category.sources - -import android.app.Dialog -import android.os.Bundle -import com.bluelinelabs.conductor.Controller -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput - -/** - * Dialog to create a new category for the library. - */ -class SourceCategoryCreateDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T : SourceCategoryCreateDialog.Listener { - - /** - * Name of the new category. Value updated with each input from the user. - */ - private var currentName = "" - - constructor(target: T) : this() { - targetController = target - } - - /** - * Called when creating the dialog for this controller. - * - * @param savedViewState The saved state of this dialog. - * @return a new dialog instance. - */ - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.action_add_category) - .setNegativeButton(android.R.string.cancel, null) - .setTextInput( - hint = resources?.getString(R.string.name), - prefill = currentName, - ) { input -> - currentName = input - } - .setPositiveButton(android.R.string.ok) { _, _ -> - (targetController as? Listener)?.createCategory(currentName) - } - .create() - } - - interface Listener { - fun createCategory(name: String) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryHolder.kt deleted file mode 100644 index 7bd8a12cb..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryHolder.kt +++ /dev/null @@ -1,31 +0,0 @@ -package eu.kanade.tachiyomi.ui.category.sources - -import android.view.View -import androidx.core.view.isVisible -import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.tachiyomi.databinding.CategoriesItemBinding -import eu.kanade.tachiyomi.util.system.dpToPx - -/** - * Holder used to display category items. - * - * @param view The view used by category items. - * @param adapter The adapter containing this holder. - */ -class SourceCategoryHolder(view: View, val adapter: SourceCategoryAdapter) : FlexibleViewHolder(view, adapter) { - - private val binding = CategoriesItemBinding.bind(view) - - /** - * Binds this holder with the given category. - * - * @param category The category to bind. - */ - fun bind(category: String) { - binding.innerContainer.minimumHeight = 60.dpToPx - - // Set capitalized title. - binding.title.text = category - binding.reorder.isVisible = false - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryItem.kt deleted file mode 100644 index de61d9179..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryItem.kt +++ /dev/null @@ -1,62 +0,0 @@ -package eu.kanade.tachiyomi.ui.category.sources - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R - -/** - * Category item for a recycler view. - */ -class SourceCategoryItem(val category: String) : AbstractFlexibleItem() { - - /** - * Whether this item is currently selected. - */ - var isSelected = false - - /** - * Returns the layout resource for this item. - */ - override fun getLayoutRes(): Int { - return R.layout.categories_item - } - - /** - * Returns a new view holder for this item. - * - * @param view The view of this item. - * @param adapter The adapter of this item. - */ - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): SourceCategoryHolder { - return SourceCategoryHolder(view, adapter as SourceCategoryAdapter) - } - - /** - * Binds the given view holder with this item. - * - * @param adapter The adapter of this item. - * @param holder The holder to bind. - * @param position The position of this item in the adapter. - * @param payloads List of partial changes. - */ - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: SourceCategoryHolder, - position: Int, - payloads: List?, - ) { - holder.bind(category) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - return false - } - - override fun hashCode(): Int { - return category.hashCode() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryPresenter.kt index cf93bf6a8..3872400b0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryPresenter.kt @@ -1,44 +1,35 @@ package eu.kanade.tachiyomi.ui.category.sources -import android.os.Bundle -import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import eu.kanade.domain.source.interactor.CreateSourceCategory +import eu.kanade.domain.source.interactor.DeleteSourceCategory +import eu.kanade.domain.source.interactor.GetSourceCategories +import eu.kanade.domain.source.interactor.RenameSourceCategory import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import rx.Observable -import rx.android.schedulers.AndroidSchedulers +import eu.kanade.tachiyomi.util.lang.launchIO +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.consumeAsFlow import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get /** * Presenter of [SourceCategoryController]. Used to manage the categories of the library. */ -class SourceCategoryPresenter : BasePresenter() { +class SourceCategoryPresenter( + private val getSourceCategories: GetSourceCategories = Injekt.get(), + private val createSourceCategory: CreateSourceCategory = Injekt.get(), + private val renameSourceCategory: RenameSourceCategory = Injekt.get(), + private val deleteSourceCategory: DeleteSourceCategory = Injekt.get(), +) : BasePresenter() { - /** - * List containing categories. - */ - private var categories: List = emptyList() + var dialog: Dialog? by mutableStateOf(null) - val preferences: PreferencesHelper = Injekt.get() + val categories = getSourceCategories.subscribe() - /** - * Called when the presenter is created. - * - * @param savedState The saved state of this presenter. - */ - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - preferences.sourcesTabCategories().asFlow().onEach { categories -> - this.categories = categories.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it })) - - Observable.just(this.categories) - .map { it.map(::SourceCategoryItem) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(SourceCategoryController::setCategories) - }.launchIn(presenterScope) - } + private val _events: Channel = Channel(Int.MAX_VALUE) + val events = _events.consumeAsFlow() /** * Creates and adds a new category to the database. @@ -46,22 +37,13 @@ class SourceCategoryPresenter : BasePresenter() { * @param name The name of the category to create. */ fun createCategory(name: String) { - // Do not allow duplicate categories. - if (categoryExists(name)) { - Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() }) - return + presenterScope.launchIO { + when (createSourceCategory.await(name)) { + is CreateSourceCategory.Result.CategoryExists -> _events.send(Event.CategoryExists) + is CreateSourceCategory.Result.InvalidName -> _events.send(Event.InvalidName) + else -> {} + } } - - if (name.contains("|")) { - Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryInvalidNameError() }) - return - } - - // Create category. - val newCategories = categories.toMutableList() - newCategories += name - - preferences.sourcesTabCategories().set(newCategories.toSet()) } /** @@ -69,56 +51,37 @@ class SourceCategoryPresenter : BasePresenter() { * * @param categories The list of categories to delete. */ - fun deleteCategories(categories: List) { - var sources = preferences.sourcesTabSourcesInCategories().get().toList() - - sources = sources.map { it.split("|") }.filterNot { it[1] in categories }.map { it[0] + "|" + it[1] } - - preferences.sourcesTabSourcesInCategories().set(sources.toSet()) - preferences.sourcesTabCategories().set( - this.categories.filterNot { it in categories }.toSet(), - ) + fun deleteCategory(categories: String) { + presenterScope.launchIO { + deleteSourceCategory.await(categories) + } } /** * Renames a category. * - * @param category The category to rename. - * @param name The new name of the category. + * @param categoryOld The category to rename. + * @param categoryNew The new name of the category. */ fun renameCategory(categoryOld: String, categoryNew: String) { - // Do not allow duplicate categories. - if (categoryExists(categoryNew)) { - Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() }) - return - } - - if (categoryNew.contains("|")) { - Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryInvalidNameError() }) - return - } - - val newCategories = categories.filterNot { it in categoryOld }.toMutableList() - newCategories += categoryNew - - var sources = preferences.sourcesTabSourcesInCategories().get().toList() - - sources = sources.map { it.split("|").toMutableList() } - .map { - if (it[1] == categoryOld) { - it[1] = categoryNew - } - it[0] + "|" + it[1] + presenterScope.launchIO { + when (renameSourceCategory.await(categoryOld, categoryNew)) { + is CreateSourceCategory.Result.CategoryExists -> _events.send(Event.CategoryExists) + is CreateSourceCategory.Result.InvalidName -> _events.send(Event.InvalidName) + else -> {} } - - preferences.sourcesTabSourcesInCategories().set(sources.toSet()) - preferences.sourcesTabCategories().set(newCategories.sorted().toSet()) + } } - /** - * Returns true if a category with the given name already exists. - */ - private fun categoryExists(name: String): Boolean { - return categories.any { it.equals(name, true) } + sealed class Event { + object CategoryExists : Event() + object InvalidName : Event() + object InternalError : Event() + } + + sealed class Dialog { + object Create : Dialog() + data class Rename(val category: String) : Dialog() + data class Delete(val category: String) : Dialog() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryRenameDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryRenameDialog.kt deleted file mode 100644 index 4a8786a8b..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/sources/SourceCategoryRenameDialog.kt +++ /dev/null @@ -1,87 +0,0 @@ -package eu.kanade.tachiyomi.ui.category.sources - -import android.app.Dialog -import android.os.Bundle -import com.bluelinelabs.conductor.Controller -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput - -/** - * Dialog to rename an existing category of the library. - */ -class SourceCategoryRenameDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T : SourceCategoryRenameDialog.Listener { - - private var category: String? = null - - /** - * Name of the new category. Value updated with each input from the user. - */ - private var currentName = "" - - constructor(target: T, category: String) : this() { - targetController = target - this.category = category - currentName = category - } - - /** - * Called when creating the dialog for this controller. - * - * @param savedViewState The saved state of this dialog. - * @return a new dialog instance. - */ - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.action_rename_category) - .setTextInput( - hint = resources?.getString(R.string.name), - prefill = currentName, - ) { input -> - currentName = input - } - .setPositiveButton(android.R.string.ok) { _, _ -> onPositive() } - .setNegativeButton(android.R.string.cancel, null) - .create() - } - - /** - * Called to save this Controller's state in the event that its host Activity is destroyed. - * - * @param outState The Bundle into which data should be saved - */ - override fun onSaveInstanceState(outState: Bundle) { - outState.putSerializable(CATEGORY_KEY, category) - super.onSaveInstanceState(outState) - } - - /** - * Restores data that was saved in the [onSaveInstanceState] method. - * - * @param savedInstanceState The bundle that has data to be restored - */ - override fun onRestoreInstanceState(savedInstanceState: Bundle) { - super.onRestoreInstanceState(savedInstanceState) - category = savedInstanceState.getSerializable(CATEGORY_KEY) as? String - } - - /** - * Called when the positive button of the dialog is clicked. - */ - private fun onPositive() { - val target = targetController as? Listener ?: return - val category = category ?: return - - target.renameCategory(category, currentName) - } - - interface Listener { - fun renameCategory(category: String, name: String) - } - - private companion object { - const val CATEGORY_KEY = "CategoryRenameDialog.category" - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt index 4abdc1f38..a19ba5fa9 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt @@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.Filter -import eu.kanade.tachiyomi.ui.category.CategoryAdapter import eu.kanade.tachiyomi.util.lang.withUIContext import exh.log.xLogW import exh.metadata.sql.models.SearchTag @@ -70,10 +69,6 @@ class LibraryCategoryAdapter(view: LibraryCategoryView, val controller: LibraryC */ private var mangas: List = emptyList() - // SY --> - val onItemReleaseListener: CategoryAdapter.OnItemReleaseListener = view - // SY <-- - /** * Sets a list of manga in the adapter. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt index 059add88b..c02a4ee73 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt @@ -20,7 +20,6 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.preference.PreferenceValues import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding -import eu.kanade.tachiyomi.ui.category.CategoryAdapter import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting import eu.kanade.tachiyomi.ui.main.MainActivity @@ -53,8 +52,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener, // SY --> - FlexibleAdapter.OnItemMoveListener, - CategoryAdapter.OnItemReleaseListener { + FlexibleAdapter.OnItemMoveListener { // SY <-- private val scope = MainScope() @@ -411,10 +409,6 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att else -> null } - override fun onItemReleased(position: Int) { - return - } - override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean { if (adapter.isSelected(fromPosition)) toggleSelection(fromPosition) return true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt index ede6bf462..6a93ec8e5 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt @@ -28,16 +28,6 @@ abstract class LibraryHolder( abstract fun onSetValues(item: LibraryItem) // SY --> - /** - * Called when an item is released. - * - * @param position The position of the released item. - */ - override fun onItemReleased(position: Int) { - super.onItemReleased(position) - (adapter as? LibraryCategoryAdapter)?.onItemReleaseListener?.onItemReleased(position) - } - override fun onLongClick(view: View?): Boolean { return if (adapter.isLongPressDragEnabled) { super.onLongClick(view) diff --git a/app/src/main/java/exh/GalleryAdder.kt b/app/src/main/java/exh/GalleryAdder.kt index 1d5ae965b..710c833f4 100755 --- a/app/src/main/java/exh/GalleryAdder.kt +++ b/app/src/main/java/exh/GalleryAdder.kt @@ -123,8 +123,8 @@ class GalleryAdder( insertManga.await( Manga.create().copy( source = source.id, - url = cleanedMangaUrl - ) + url = cleanedMangaUrl, + ), ) getManga.await(cleanedMangaUrl, source.id)!! } diff --git a/app/src/main/java/exh/favorites/FavoritesSyncHelper.kt b/app/src/main/java/exh/favorites/FavoritesSyncHelper.kt index 66605d42c..b862aac41 100644 --- a/app/src/main/java/exh/favorites/FavoritesSyncHelper.kt +++ b/app/src/main/java/exh/favorites/FavoritesSyncHelper.kt @@ -3,8 +3,8 @@ package exh.favorites import android.content.Context import android.net.wifi.WifiManager import android.os.PowerManager +import eu.kanade.domain.category.interactor.CreateCategoryWithName import eu.kanade.domain.category.interactor.GetCategories -import eu.kanade.domain.category.interactor.InsertCategory import eu.kanade.domain.category.interactor.SetMangaCategories import eu.kanade.domain.category.interactor.UpdateCategory import eu.kanade.domain.category.model.Category @@ -55,7 +55,7 @@ class FavoritesSyncHelper(val context: Context) { private val getManga: GetManga by injectLazy() private val updateManga: UpdateManga by injectLazy() private val setMangaCategories: SetMangaCategories by injectLazy() - private val insertCategory: InsertCategory by injectLazy() + private val createCategoryWithName: CreateCategoryWithName by injectLazy() private val updateCategory: UpdateCategory by injectLazy() private val prefs: PreferencesHelper by injectLazy() @@ -208,9 +208,10 @@ class FavoritesSyncHelper(val context: Context) { categories.forEachIndexed { index, remote -> val local = localCategories.getOrElse(index) { - when (val insertCategoryResult = insertCategory.await(remote, index.toLong())) { - is InsertCategory.Result.Error -> throw insertCategoryResult.error - is InsertCategory.Result.Success -> Category(insertCategoryResult.id, remote, index.toLong(), 0L, emptyList()) + when (val createCategoryWithNameResult = createCategoryWithName.await(remote)) { + is CreateCategoryWithName.Result.InternalError -> throw createCategoryWithNameResult.error + CreateCategoryWithName.Result.NameAlreadyExistsError -> throw IllegalStateException("Category $remote already exists") + is CreateCategoryWithName.Result.Success -> createCategoryWithNameResult.category } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 54d117847..11f36f6a7 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -849,4 +849,9 @@ Navigate to pan Zoom landscape image Unable to open last read chapter + Do you wish to delete the category %s + Delete category + Yes + No + InternalError: Check crash logs for further information diff --git a/app/src/main/res/values/strings_sy.xml b/app/src/main/res/values/strings_sy.xml index 4fbc08c7e..4992eedde 100644 --- a/app/src/main/res/values/strings_sy.xml +++ b/app/src/main/res/values/strings_sy.xml @@ -381,6 +381,8 @@ You have no tags. Tap the plus button to create one for sorting your library by tags This tag exists! Tags deleted + Delete tag + Do you wish to delete the tag %s Redundant @@ -399,6 +401,8 @@ Repo deleted Invalid repo name Repo source + Delete repo + Do you wish to delete the repo %s Select sources