diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ServerListeners.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ServerListeners.kt index 73fee015..dec1636b 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ServerListeners.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ServerListeners.kt @@ -23,103 +23,102 @@ import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class ServerListeners - @Inject - constructor() { - val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) +@Inject +class ServerListeners { + val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - private fun Flow.startWith(value: T) = onStart { emit(value) } + private fun Flow.startWith(value: T) = onStart { emit(value) } - private val _mangaListener = MutableSharedFlow>( - extraBufferCapacity = Channel.UNLIMITED, - ) - val mangaListener = _mangaListener.asSharedFlow() + private val _mangaListener = MutableSharedFlow>( + extraBufferCapacity = Channel.UNLIMITED, + ) + val mangaListener = _mangaListener.asSharedFlow() - private val _chapterIdsListener = MutableSharedFlow>( - extraBufferCapacity = Channel.UNLIMITED, - ) - val chapterIdsListener = _chapterIdsListener.asSharedFlow() + private val _chapterIdsListener = MutableSharedFlow>( + extraBufferCapacity = Channel.UNLIMITED, + ) + val chapterIdsListener = _chapterIdsListener.asSharedFlow() - private val _mangaChapterIdsListener = MutableSharedFlow>( - extraBufferCapacity = Channel.UNLIMITED, - ) - val mangaChapterIdsListener = _mangaChapterIdsListener.asSharedFlow() + private val _mangaChapterIdsListener = MutableSharedFlow>( + extraBufferCapacity = Channel.UNLIMITED, + ) + val mangaChapterIdsListener = _mangaChapterIdsListener.asSharedFlow() - private val categoryMangaListener = MutableSharedFlow( - extraBufferCapacity = Channel.UNLIMITED, - ) + private val categoryMangaListener = MutableSharedFlow( + extraBufferCapacity = Channel.UNLIMITED, + ) - private val extensionListener = MutableSharedFlow>( - extraBufferCapacity = Channel.UNLIMITED, - ) + private val extensionListener = MutableSharedFlow>( + extraBufferCapacity = Channel.UNLIMITED, + ) - fun combineMangaUpdates( - flow: Flow, - predate: (suspend (List) -> Boolean)? = null, - ) = if (predate != null) { - _mangaListener - .filter(predate) - .startWith(Unit) - } else { - _mangaListener.startWith(Unit) - } - .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - .flatMapLatest { flow } + fun combineMangaUpdates( + flow: Flow, + predate: (suspend (List) -> Boolean)? = null, + ) = if (predate != null) { + _mangaListener + .filter(predate) + .startWith(Unit) + } else { + _mangaListener.startWith(Unit) + } + .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + .flatMapLatest { flow } - fun updateManga(vararg ids: Long) { - scope.launch { - _mangaListener.emit(ids.toList()) - } - } - - fun combineCategoryManga( - flow: Flow, - predate: (suspend (Long) -> Boolean)? = null, - ) = if (predate != null) { - categoryMangaListener.filter(predate).startWith(-1) - } else { - categoryMangaListener.startWith(-1) - } - .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - .flatMapLatest { flow } - - fun updateCategoryManga(id: Long) { - scope.launch { - categoryMangaListener.emit(id) - } - } - - fun combineChapters( - flow: Flow, - chapterIdPredate: (suspend (List) -> Boolean)? = null, - mangaIdPredate: (suspend (List) -> Boolean)? = null, - ): Flow { - val idsListener = _chapterIdsListener - .filter { chapterIdPredate?.invoke(it) ?: false } - .startWith(Unit) - .combine( - _mangaChapterIdsListener.filter { mangaIdPredate?.invoke(it) ?: false } - .startWith(Unit), - ) { _, _ -> } - - return idsListener - .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - .flatMapLatest { flow } - } - - fun updateChapters(chapterIds: List) { - scope.launch { - _chapterIdsListener.emit(chapterIds) - } - } - - fun updateChapters(vararg chapterIds: Long) { - scope.launch { - _chapterIdsListener.emit(chapterIds.toList()) - } - } - - companion object { - private val log = logging() + fun updateManga(vararg ids: Long) { + scope.launch { + _mangaListener.emit(ids.toList()) } } + + fun combineCategoryManga( + flow: Flow, + predate: (suspend (Long) -> Boolean)? = null, + ) = if (predate != null) { + categoryMangaListener.filter(predate).startWith(-1) + } else { + categoryMangaListener.startWith(-1) + } + .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + .flatMapLatest { flow } + + fun updateCategoryManga(id: Long) { + scope.launch { + categoryMangaListener.emit(id) + } + } + + fun combineChapters( + flow: Flow, + chapterIdPredate: (suspend (List) -> Boolean)? = null, + mangaIdPredate: (suspend (List) -> Boolean)? = null, + ): Flow { + val idsListener = _chapterIdsListener + .filter { chapterIdPredate?.invoke(it) ?: false } + .startWith(Unit) + .combine( + _mangaChapterIdsListener.filter { mangaIdPredate?.invoke(it) ?: false } + .startWith(Unit), + ) { _, _ -> } + + return idsListener + .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + .flatMapLatest { flow } + } + + fun updateChapters(chapterIds: List) { + scope.launch { + _chapterIdsListener.emit(chapterIds) + } + } + + fun updateChapters(vararg chapterIds: Long) { + scope.launch { + _chapterIdsListener.emit(chapterIds.toList()) + } + } + + companion object { + private val log = logging() + } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ExportBackupFile.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ExportBackupFile.kt index 548cfab1..463a88f2 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ExportBackupFile.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ExportBackupFile.kt @@ -13,30 +13,29 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class ExportBackupFile - @Inject - constructor( - private val backupRepository: BackupRepository, - ) { - suspend fun await( - includeCategories: Boolean, - includeChapters: Boolean, - block: HttpRequestBuilder.() -> Unit = {}, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(includeCategories, includeChapters, block) - .catch { - onError(it) - log.warn(it) { "Failed to export backup" } - } - .singleOrNull() - - fun asFlow( - includeCategories: Boolean, - includeChapters: Boolean, - block: HttpRequestBuilder.() -> Unit = {}, - ) = backupRepository.createBackup(includeCategories, includeChapters, block) - - companion object { - private val log = logging() +@Inject +class ExportBackupFile( + private val backupRepository: BackupRepository, +) { + suspend fun await( + includeCategories: Boolean, + includeChapters: Boolean, + block: HttpRequestBuilder.() -> Unit = {}, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(includeCategories, includeChapters, block) + .catch { + onError(it) + log.warn(it) { "Failed to export backup" } } + .singleOrNull() + + fun asFlow( + includeCategories: Boolean, + includeChapters: Boolean, + block: HttpRequestBuilder.() -> Unit = {}, + ) = backupRepository.createBackup(includeCategories, includeChapters, block) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ImportBackupFile.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ImportBackupFile.kt index 19f3f32a..c8af3907 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ImportBackupFile.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ImportBackupFile.kt @@ -15,24 +15,23 @@ import okio.Path import okio.SYSTEM import org.lighthousegames.logging.logging -class ImportBackupFile - @Inject - constructor( - private val backupRepository: BackupRepository, - ) { - suspend fun await( - file: Path, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(file) - .catch { - onError(it) - log.warn(it) { "Failed to import backup ${file.name}" } - } - .singleOrNull() - - fun asFlow(file: Path) = backupRepository.restoreBackup(FileSystem.SYSTEM.source(file)) - - companion object { - private val log = logging() +@Inject +class ImportBackupFile( + private val backupRepository: BackupRepository, +) { + suspend fun await( + file: Path, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(file) + .catch { + onError(it) + log.warn(it) { "Failed to import backup ${file.name}" } } + .singleOrNull() + + fun asFlow(file: Path) = backupRepository.restoreBackup(FileSystem.SYSTEM.source(file)) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ValidateBackupFile.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ValidateBackupFile.kt index 453f45db..32960136 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ValidateBackupFile.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ValidateBackupFile.kt @@ -15,24 +15,23 @@ import okio.Path import okio.SYSTEM import org.lighthousegames.logging.logging -class ValidateBackupFile - @Inject - constructor( - private val backupRepository: BackupRepository, - ) { - suspend fun await( - file: Path, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(file) - .catch { - onError(it) - log.warn(it) { "Failed to validate backup ${file.name}" } - } - .singleOrNull() - - fun asFlow(file: Path) = backupRepository.validateBackup(FileSystem.SYSTEM.source(file)) - - companion object { - private val log = logging() +@Inject +class ValidateBackupFile( + private val backupRepository: BackupRepository, +) { + suspend fun await( + file: Path, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(file) + .catch { + onError(it) + log.warn(it) { "Failed to validate backup ${file.name}" } } + .singleOrNull() + + fun asFlow(file: Path) = backupRepository.validateBackup(FileSystem.SYSTEM.source(file)) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/AddMangaToCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/AddMangaToCategory.kt index 78bd23bc..1485939b 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/AddMangaToCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/AddMangaToCategory.kt @@ -17,61 +17,60 @@ import kotlinx.coroutines.flow.map import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class AddMangaToCategory - @Inject - constructor( - private val categoryRepository: CategoryRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - mangaId: Long, - categoryId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId, categoryId) - .catch { - onError(it) - log.warn(it) { "Failed to add $mangaId to category $categoryId" } - } - .collect() - - suspend fun await( - manga: Manga, - category: Category, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga, category) - .catch { - onError(it) - log.warn(it) { "Failed to add ${manga.title}(${manga.id}) to category ${category.name}" } - } - .collect() - - fun asFlow( - mangaId: Long, - categoryId: Long, - ) = if (categoryId != 0L) { - categoryRepository.addMangaToCategory(mangaId, categoryId) - .map { serverListeners.updateCategoryManga(categoryId) } - } else { - flow { - serverListeners.updateCategoryManga(categoryId) - emit(Unit) - } +@Inject +class AddMangaToCategory( + private val categoryRepository: CategoryRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + mangaId: Long, + categoryId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, categoryId) + .catch { + onError(it) + log.warn(it) { "Failed to add $mangaId to category $categoryId" } } + .collect() - fun asFlow( - manga: Manga, - category: Category, - ) = if (category.id != 0L) { - categoryRepository.addMangaToCategory(manga.id, category.id) - .map { serverListeners.updateCategoryManga(category.id) } - } else { - flow { - serverListeners.updateCategoryManga(category.id) - emit(Unit) - } + suspend fun await( + manga: Manga, + category: Category, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, category) + .catch { + onError(it) + log.warn(it) { "Failed to add ${manga.title}(${manga.id}) to category ${category.name}" } } + .collect() - companion object { - private val log = logging() + fun asFlow( + mangaId: Long, + categoryId: Long, + ) = if (categoryId != 0L) { + categoryRepository.addMangaToCategory(mangaId, categoryId) + .map { serverListeners.updateCategoryManga(categoryId) } + } else { + flow { + serverListeners.updateCategoryManga(categoryId) + emit(Unit) } } + + fun asFlow( + manga: Manga, + category: Category, + ) = if (category.id != 0L) { + categoryRepository.addMangaToCategory(manga.id, category.id) + .map { serverListeners.updateCategoryManga(category.id) } + } else { + flow { + serverListeners.updateCategoryManga(category.id) + emit(Unit) + } + } + + companion object { + private val log = logging() + } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/CreateCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/CreateCategory.kt index 80c36d7a..6ed047f0 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/CreateCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/CreateCategory.kt @@ -12,24 +12,23 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class CreateCategory - @Inject - constructor( - private val categoryRepository: CategoryRepository, - ) { - suspend fun await( - name: String, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(name) - .catch { - onError(it) - log.warn(it) { "Failed to create category $name" } - } - .collect() - - fun asFlow(name: String) = categoryRepository.createCategory(name) - - companion object { - private val log = logging() +@Inject +class CreateCategory( + private val categoryRepository: CategoryRepository, +) { + suspend fun await( + name: String, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(name) + .catch { + onError(it) + log.warn(it) { "Failed to create category $name" } } + .collect() + + fun asFlow(name: String) = categoryRepository.createCategory(name) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/DeleteCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/DeleteCategory.kt index 0704cc75..b6132438 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/DeleteCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/DeleteCategory.kt @@ -13,36 +13,35 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class DeleteCategory - @Inject - constructor( - private val categoryRepository: CategoryRepository, - ) { - suspend fun await( - categoryId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(categoryId) - .catch { - onError(it) - log.warn(it) { "Failed to delete category $categoryId" } - } - .collect() - - suspend fun await( - category: Category, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(category) - .catch { - onError(it) - log.warn(it) { "Failed to delete category ${category.name}" } - } - .collect() - - fun asFlow(categoryId: Long) = categoryRepository.deleteCategory(categoryId) - - fun asFlow(category: Category) = categoryRepository.deleteCategory(category.id) - - companion object { - private val log = logging() +@Inject +class DeleteCategory( + private val categoryRepository: CategoryRepository, +) { + suspend fun await( + categoryId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(categoryId) + .catch { + onError(it) + log.warn(it) { "Failed to delete category $categoryId" } } + .collect() + + suspend fun await( + category: Category, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(category) + .catch { + onError(it) + log.warn(it) { "Failed to delete category ${category.name}" } + } + .collect() + + fun asFlow(categoryId: Long) = categoryRepository.deleteCategory(categoryId) + + fun asFlow(category: Category) = categoryRepository.deleteCategory(category.id) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetCategories.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetCategories.kt index 7841d13a..6f6b1d99 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetCategories.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetCategories.kt @@ -13,32 +13,31 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetCategories - @Inject - constructor( - private val categoryRepository: CategoryRepository, - ) { - suspend fun await( - dropDefault: Boolean = false, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(dropDefault) - .catch { - onError(it) - log.warn(it) { "Failed to get categories" } - } - .singleOrNull() - - fun asFlow(dropDefault: Boolean = false) = - categoryRepository.getCategories() - .map { categories -> - if (dropDefault) { - categories.filterNot { it.name.equals("default", true) } - } else { - categories - } - } - - companion object { - private val log = logging() +@Inject +class GetCategories( + private val categoryRepository: CategoryRepository, +) { + suspend fun await( + dropDefault: Boolean = false, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(dropDefault) + .catch { + onError(it) + log.warn(it) { "Failed to get categories" } } + .singleOrNull() + + fun asFlow(dropDefault: Boolean = false) = + categoryRepository.getCategories() + .map { categories -> + if (dropDefault) { + categories.filterNot { it.name.equals("default", true) } + } else { + categories + } + } + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaCategories.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaCategories.kt index 1ed44476..6c8938c0 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaCategories.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaCategories.kt @@ -13,36 +13,35 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetMangaCategories - @Inject - constructor( - private val categoryRepository: CategoryRepository, - ) { - suspend fun await( - mangaId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId) - .catch { - onError(it) - log.warn(it) { "Failed to get categories for $mangaId" } - } - .singleOrNull() - - suspend fun await( - manga: Manga, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga) - .catch { - onError(it) - log.warn(it) { "Failed to get categories for ${manga.title}(${manga.id})" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = categoryRepository.getMangaCategories(mangaId) - - fun asFlow(manga: Manga) = categoryRepository.getMangaCategories(manga.id) - - companion object { - private val log = logging() +@Inject +class GetMangaCategories( + private val categoryRepository: CategoryRepository, +) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .catch { + onError(it) + log.warn(it) { "Failed to get categories for $mangaId" } } + .singleOrNull() + + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .catch { + onError(it) + log.warn(it) { "Failed to get categories for ${manga.title}(${manga.id})" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = categoryRepository.getMangaCategories(mangaId) + + fun asFlow(manga: Manga) = categoryRepository.getMangaCategories(manga.id) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaListFromCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaListFromCategory.kt index 2f8d0559..78b8ed72 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaListFromCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaListFromCategory.kt @@ -15,45 +15,44 @@ import kotlinx.coroutines.flow.take import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetMangaListFromCategory - @Inject - constructor( - private val categoryRepository: CategoryRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - categoryId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(categoryId) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get manga list from category $categoryId" } - } - .singleOrNull() - - suspend fun await( - category: Category, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(category) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get manga list from category ${category.name}" } - } - .singleOrNull() - - fun asFlow(categoryId: Long) = - serverListeners.combineCategoryManga( - categoryRepository.getMangaFromCategory(categoryId), - ) { categoryId == it } - - fun asFlow(category: Category) = - serverListeners.combineCategoryManga( - categoryRepository.getMangaFromCategory(category.id), - ) { category.id == it } - - companion object { - private val log = logging() +@Inject +class GetMangaListFromCategory( + private val categoryRepository: CategoryRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + categoryId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(categoryId) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get manga list from category $categoryId" } } + .singleOrNull() + + suspend fun await( + category: Category, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(category) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get manga list from category ${category.name}" } + } + .singleOrNull() + + fun asFlow(categoryId: Long) = + serverListeners.combineCategoryManga( + categoryRepository.getMangaFromCategory(categoryId), + ) { categoryId == it } + + fun asFlow(category: Category) = + serverListeners.combineCategoryManga( + categoryRepository.getMangaFromCategory(category.id), + ) { category.id == it } + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ModifyCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ModifyCategory.kt index 7864b9c1..33ce9d85 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ModifyCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ModifyCategory.kt @@ -13,52 +13,51 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class ModifyCategory - @Inject - constructor( - private val categoryRepository: CategoryRepository, - ) { - suspend fun await( - categoryId: Long, - name: String, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow( - categoryId = categoryId, - name = name, - ).catch { - onError(it) - log.warn(it) { "Failed to modify category $categoryId with options: name=$name" } - }.collect() +@Inject +class ModifyCategory( + private val categoryRepository: CategoryRepository, +) { + suspend fun await( + categoryId: Long, + name: String, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow( + categoryId = categoryId, + name = name, + ).catch { + onError(it) + log.warn(it) { "Failed to modify category $categoryId with options: name=$name" } + }.collect() - suspend fun await( - category: Category, - name: String? = null, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow( - category = category, - name = name, - ).catch { - onError(it) - log.warn(it) { "Failed to modify category ${category.name} with options: name=$name" } - }.collect() + suspend fun await( + category: Category, + name: String? = null, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow( + category = category, + name = name, + ).catch { + onError(it) + log.warn(it) { "Failed to modify category ${category.name} with options: name=$name" } + }.collect() - fun asFlow( - categoryId: Long, - name: String, - ) = categoryRepository.modifyCategory( - categoryId = categoryId, - name = name, - ) + fun asFlow( + categoryId: Long, + name: String, + ) = categoryRepository.modifyCategory( + categoryId = categoryId, + name = name, + ) - fun asFlow( - category: Category, - name: String? = null, - ) = categoryRepository.modifyCategory( - categoryId = category.id, - name = name ?: category.name, - ) + fun asFlow( + category: Category, + name: String? = null, + ) = categoryRepository.modifyCategory( + categoryId = category.id, + name = name ?: category.name, + ) - companion object { - private val log = logging() - } + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/RemoveMangaFromCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/RemoveMangaFromCategory.kt index e69fa7b0..748d3fcf 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/RemoveMangaFromCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/RemoveMangaFromCategory.kt @@ -17,61 +17,60 @@ import kotlinx.coroutines.flow.map import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class RemoveMangaFromCategory - @Inject - constructor( - private val categoryRepository: CategoryRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - mangaId: Long, - categoryId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId, categoryId) - .catch { - onError(it) - log.warn(it) { "Failed to remove $mangaId from category $categoryId" } - } - .collect() - - suspend fun await( - manga: Manga, - category: Category, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga, category) - .catch { - onError(it) - log.warn(it) { "Failed to remove ${manga.title}(${manga.id}) from category ${category.name}" } - } - .collect() - - fun asFlow( - mangaId: Long, - categoryId: Long, - ) = if (categoryId != 0L) { - categoryRepository.removeMangaFromCategory(mangaId, categoryId) - .map { serverListeners.updateCategoryManga(categoryId) } - } else { - flow { - serverListeners.updateCategoryManga(categoryId) - emit(Unit) - } +@Inject +class RemoveMangaFromCategory( + private val categoryRepository: CategoryRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + mangaId: Long, + categoryId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, categoryId) + .catch { + onError(it) + log.warn(it) { "Failed to remove $mangaId from category $categoryId" } } + .collect() - fun asFlow( - manga: Manga, - category: Category, - ) = if (category.id != 0L) { - categoryRepository.removeMangaFromCategory(manga.id, category.id) - .map { serverListeners.updateCategoryManga(category.id) } - } else { - flow { - serverListeners.updateCategoryManga(category.id) - emit(Unit) - } + suspend fun await( + manga: Manga, + category: Category, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, category) + .catch { + onError(it) + log.warn(it) { "Failed to remove ${manga.title}(${manga.id}) from category ${category.name}" } } + .collect() - companion object { - private val log = logging() + fun asFlow( + mangaId: Long, + categoryId: Long, + ) = if (categoryId != 0L) { + categoryRepository.removeMangaFromCategory(mangaId, categoryId) + .map { serverListeners.updateCategoryManga(categoryId) } + } else { + flow { + serverListeners.updateCategoryManga(categoryId) + emit(Unit) } } + + fun asFlow( + manga: Manga, + category: Category, + ) = if (category.id != 0L) { + categoryRepository.removeMangaFromCategory(manga.id, category.id) + .map { serverListeners.updateCategoryManga(category.id) } + } else { + flow { + serverListeners.updateCategoryManga(category.id) + emit(Unit) + } + } + + companion object { + private val log = logging() + } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ReorderCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ReorderCategory.kt index db3b1bc6..317c90b8 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ReorderCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ReorderCategory.kt @@ -12,28 +12,27 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class ReorderCategory - @Inject - constructor( - private val categoryRepository: CategoryRepository, - ) { - suspend fun await( - categoryId: Long, - position: Int, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(categoryId, position) - .catch { - onError(it) - log.warn(it) { "Failed to move category $categoryId to $position" } - } - .collect() - - fun asFlow( - categoryId: Long, - position: Int, - ) = categoryRepository.reorderCategory(categoryId, position) - - companion object { - private val log = logging() +@Inject +class ReorderCategory( + private val categoryRepository: CategoryRepository, +) { + suspend fun await( + categoryId: Long, + position: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(categoryId, position) + .catch { + onError(it) + log.warn(it) { "Failed to move category $categoryId to $position" } } + .collect() + + fun asFlow( + categoryId: Long, + position: Int, + ) = categoryRepository.reorderCategory(categoryId, position) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/UpdateCategoryMeta.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/UpdateCategoryMeta.kt index 21f91de9..21df6152 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/UpdateCategoryMeta.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/UpdateCategoryMeta.kt @@ -14,37 +14,36 @@ import kotlinx.coroutines.flow.flow import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateCategoryMeta - @Inject - constructor( - private val categoryRepository: CategoryRepository, - ) { - suspend fun await( - category: Category, - example: Int = category.meta.example, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(category, example) - .catch { - onError(it) - log.warn(it) { "Failed to update ${category.name}(${category.id}) meta" } - } - .collect() - - fun asFlow( - category: Category, - example: Int = category.meta.example, - ) = flow { - if (example != category.meta.example) { - categoryRepository.updateCategoryMeta( - category.id, - "example", - example.toString(), - ).collect() - } - emit(Unit) +@Inject +class UpdateCategoryMeta( + private val categoryRepository: CategoryRepository, +) { + suspend fun await( + category: Category, + example: Int = category.meta.example, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(category, example) + .catch { + onError(it) + log.warn(it) { "Failed to update ${category.name}(${category.id}) meta" } } + .collect() - companion object { - private val log = logging() + fun asFlow( + category: Category, + example: Int = category.meta.example, + ) = flow { + if (example != category.meta.example) { + categoryRepository.updateCategoryMeta( + category.id, + "example", + example.toString(), + ).collect() } + emit(Unit) } + + companion object { + private val log = logging() + } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/DeleteChapterDownload.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/DeleteChapterDownload.kt index fd7bcb18..e7d123c4 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/DeleteChapterDownload.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/DeleteChapterDownload.kt @@ -16,73 +16,72 @@ import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging import kotlin.jvm.JvmName -class DeleteChapterDownload - @Inject - constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - chapterId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapterId) - .catch { - onError(it) - log.warn(it) { "Failed to delete chapter download for $chapterId" } - } - .collect() - - @JvmName("awaitChapter") - suspend fun await( - chapter: Chapter, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapter) - .catch { - onError(it) - log.warn(it) { "Failed to delete chapter download for ${chapter.index} of ${chapter.mangaId}" } - } - .collect() - - suspend fun await( - chapterIds: List, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapterIds) - .catch { - onError(it) - log.warn(it) { "Failed to delete chapter download for $chapterIds" } - } - .collect() - - @JvmName("awaitChapters") - suspend fun await( - chapters: List, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapters) - .catch { - onError(it) - log.warn(it) { "Failed to delete chapter download for ${chapters.joinToString { it.id.toString() }}" } - } - .collect() - - fun asFlow(chapterId: Long) = - chapterRepository.deleteDownloadedChapter(chapterId) - .onEach { serverListeners.updateChapters(chapterId) } - - @JvmName("asFlowChapter") - fun asFlow(chapter: Chapter) = - chapterRepository.deleteDownloadedChapter(chapter.id) - .onEach { serverListeners.updateChapters(chapter.id) } - - fun asFlow(chapterIds: List) = - chapterRepository.deleteDownloadedChapters(chapterIds) - .onEach { serverListeners.updateChapters(chapterIds) } - - @JvmName("asFlowChapters") - fun asFlow(chapter: List) = - chapterRepository.deleteDownloadedChapters(chapter.map { it.id }) - .onEach { serverListeners.updateChapters(chapter.map { it.id }) } - - companion object { - private val log = logging() +@Inject +class DeleteChapterDownload( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + chapterId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapterId) + .catch { + onError(it) + log.warn(it) { "Failed to delete chapter download for $chapterId" } } + .collect() + + @JvmName("awaitChapter") + suspend fun await( + chapter: Chapter, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter) + .catch { + onError(it) + log.warn(it) { "Failed to delete chapter download for ${chapter.index} of ${chapter.mangaId}" } + } + .collect() + + suspend fun await( + chapterIds: List, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapterIds) + .catch { + onError(it) + log.warn(it) { "Failed to delete chapter download for $chapterIds" } + } + .collect() + + @JvmName("awaitChapters") + suspend fun await( + chapters: List, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapters) + .catch { + onError(it) + log.warn(it) { "Failed to delete chapter download for ${chapters.joinToString { it.id.toString() }}" } + } + .collect() + + fun asFlow(chapterId: Long) = + chapterRepository.deleteDownloadedChapter(chapterId) + .onEach { serverListeners.updateChapters(chapterId) } + + @JvmName("asFlowChapter") + fun asFlow(chapter: Chapter) = + chapterRepository.deleteDownloadedChapter(chapter.id) + .onEach { serverListeners.updateChapters(chapter.id) } + + fun asFlow(chapterIds: List) = + chapterRepository.deleteDownloadedChapters(chapterIds) + .onEach { serverListeners.updateChapters(chapterIds) } + + @JvmName("asFlowChapters") + fun asFlow(chapter: List) = + chapterRepository.deleteDownloadedChapters(chapter.map { it.id }) + .onEach { serverListeners.updateChapters(chapter.map { it.id }) } + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapter.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapter.kt index 74f8c265..d1988980 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapter.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapter.kt @@ -15,47 +15,46 @@ import kotlinx.coroutines.flow.take import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetChapter - @Inject - constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - chapterId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapterId) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get chapter $chapterId" } - } - .singleOrNull() - - suspend fun await( - chapter: Chapter, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapter) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get chapter ${chapter.index} for ${chapter.mangaId}" } - } - .singleOrNull() - - fun asFlow(chapterId: Long) = - serverListeners.combineChapters( - chapterRepository.getChapter(chapterId), - chapterIdPredate = { ids -> chapterId in ids }, - ) - - fun asFlow(chapter: Chapter) = - serverListeners.combineChapters( - chapterRepository.getChapter(chapter.id), - chapterIdPredate = { ids -> chapter.id in ids }, - ) - - companion object { - private val log = logging() +@Inject +class GetChapter( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + chapterId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapterId) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get chapter $chapterId" } } + .singleOrNull() + + suspend fun await( + chapter: Chapter, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get chapter ${chapter.index} for ${chapter.mangaId}" } + } + .singleOrNull() + + fun asFlow(chapterId: Long) = + serverListeners.combineChapters( + chapterRepository.getChapter(chapterId), + chapterIdPredate = { ids -> chapterId in ids }, + ) + + fun asFlow(chapter: Chapter) = + serverListeners.combineChapters( + chapterRepository.getChapter(chapter.id), + chapterIdPredate = { ids -> chapter.id in ids }, + ) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapterPages.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapterPages.kt index beedcbfa..c2acbbf3 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapterPages.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapterPages.kt @@ -13,40 +13,39 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetChapterPages - @Inject - constructor( - private val chapterRepository: ChapterRepository, - ) { - suspend fun await( - chapterId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapterId) - .catch { - onError(it) - log.warn(it) { "Failed to get pages for $chapterId" } - } - .singleOrNull() - - suspend fun await( - url: String, - onError: suspend (Throwable) -> Unit = {}, - block: HttpRequestBuilder.() -> Unit, - ) = asFlow(url, block) - .catch { - onError(it) - log.warn(it) { "Failed to get page $url" } - } - .singleOrNull() - - fun asFlow(chapterId: Long) = chapterRepository.getPages(chapterId) - - fun asFlow( - url: String, - block: HttpRequestBuilder.() -> Unit, - ) = chapterRepository.getPage(url, block) - - companion object { - private val log = logging() +@Inject +class GetChapterPages( + private val chapterRepository: ChapterRepository, +) { + suspend fun await( + chapterId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapterId) + .catch { + onError(it) + log.warn(it) { "Failed to get pages for $chapterId" } } + .singleOrNull() + + suspend fun await( + url: String, + onError: suspend (Throwable) -> Unit = {}, + block: HttpRequestBuilder.() -> Unit, + ) = asFlow(url, block) + .catch { + onError(it) + log.warn(it) { "Failed to get page $url" } + } + .singleOrNull() + + fun asFlow(chapterId: Long) = chapterRepository.getPages(chapterId) + + fun asFlow( + url: String, + block: HttpRequestBuilder.() -> Unit, + ) = chapterRepository.getPage(url, block) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapters.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapters.kt index b0987526..97645e52 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapters.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapters.kt @@ -15,47 +15,46 @@ import kotlinx.coroutines.flow.take import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetChapters - @Inject - constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - mangaId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get chapters for $mangaId" } - } - .singleOrNull() - - suspend fun await( - manga: Manga, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get chapters for ${manga.title}(${manga.id})" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = - serverListeners.combineChapters( - chapterRepository.getChapters(mangaId), - chapterIdPredate = { ids -> false }, // todo - ) - - fun asFlow(manga: Manga) = - serverListeners.combineChapters( - chapterRepository.getChapters(manga.id), - chapterIdPredate = { ids -> false }, // todo - ) - - companion object { - private val log = logging() +@Inject +class GetChapters( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get chapters for $mangaId" } } + .singleOrNull() + + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get chapters for ${manga.title}(${manga.id})" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = + serverListeners.combineChapters( + chapterRepository.getChapters(mangaId), + chapterIdPredate = { ids -> false }, // todo + ) + + fun asFlow(manga: Manga) = + serverListeners.combineChapters( + chapterRepository.getChapters(manga.id), + chapterIdPredate = { ids -> false }, // todo + ) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/RefreshChapters.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/RefreshChapters.kt index 3af2091f..d68868cc 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/RefreshChapters.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/RefreshChapters.kt @@ -15,41 +15,40 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class RefreshChapters - @Inject - constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - mangaId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId) - .catch { - onError(it) - log.warn(it) { "Failed to refresh chapters for $mangaId" } - } - .singleOrNull() - - suspend fun await( - manga: Manga, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga) - .catch { - onError(it) - log.warn(it) { "Failed to refresh chapters for ${manga.title}(${manga.id})" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = - chapterRepository.fetchChapters(mangaId) - .onEach { serverListeners.updateChapters(mangaId) } - - fun asFlow(manga: Manga) = - chapterRepository.fetchChapters(manga.id) - .onEach { serverListeners.updateChapters(manga.id) } - - companion object { - private val log = logging() +@Inject +class RefreshChapters( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .catch { + onError(it) + log.warn(it) { "Failed to refresh chapters for $mangaId" } } + .singleOrNull() + + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .catch { + onError(it) + log.warn(it) { "Failed to refresh chapters for ${manga.title}(${manga.id})" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = + chapterRepository.fetchChapters(mangaId) + .onEach { serverListeners.updateChapters(mangaId) } + + fun asFlow(manga: Manga) = + chapterRepository.fetchChapters(manga.id) + .onEach { serverListeners.updateChapters(manga.id) } + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapter.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapter.kt index f872bfef..dc30e184 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapter.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapter.kt @@ -16,115 +16,114 @@ import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging import kotlin.jvm.JvmName -class UpdateChapter - @Inject - constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - chapterId: Long, - bookmarked: Boolean? = null, - read: Boolean? = null, - lastPageRead: Int? = null, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapterId, bookmarked, read, lastPageRead) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter bookmark for chapter $chapterId" } - } - .collect() - - suspend fun await( - chapter: Chapter, - bookmarked: Boolean? = null, - read: Boolean? = null, - lastPageRead: Int? = null, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapter, bookmarked, read, lastPageRead) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter bookmark for chapter ${chapter.index} of ${chapter.mangaId}" } - } - .collect() - - suspend fun await( - chapterIds: List, - bookmarked: Boolean? = null, - read: Boolean? = null, - lastPageRead: Int? = null, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapterIds, bookmarked, read, lastPageRead) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter bookmark for chapters $chapterIds" } - } - .collect() - - @JvmName("awaitChapters") - suspend fun await( - chapters: List, - bookmarked: Boolean? = null, - read: Boolean? = null, - lastPageRead: Int? = null, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapters, bookmarked, read, lastPageRead) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter bookmark for chapters ${chapters.joinToString { it.id.toString() }}" } - } - .collect() - - fun asFlow( - chapterId: Long, - bookmarked: Boolean? = null, - read: Boolean? = null, - lastPageRead: Int? = null, - ) = chapterRepository.updateChapter( - chapterId = chapterId, - bookmarked = bookmarked, - read = read, - lastPageRead = lastPageRead, - ).onEach { serverListeners.updateChapters(chapterId) } - - fun asFlow( - chapter: Chapter, - bookmarked: Boolean? = null, - read: Boolean? = null, - lastPageRead: Int? = null, - ) = chapterRepository.updateChapter( - chapterId = chapter.id, - bookmarked = bookmarked, - read = read, - lastPageRead = lastPageRead, - ).onEach { serverListeners.updateChapters(chapter.id) } - - fun asFlow( - chapterIds: List, - bookmarked: Boolean? = null, - read: Boolean? = null, - lastPageRead: Int? = null, - ) = chapterRepository.updateChapters( - chapterIds = chapterIds, - bookmarked = bookmarked, - read = read, - lastPageRead = lastPageRead, - ).onEach { serverListeners.updateChapters(chapterIds) } - - @JvmName("asFlowChapters") - fun asFlow( - chapters: List, - bookmarked: Boolean? = null, - read: Boolean? = null, - lastPageRead: Int? = null, - ) = chapterRepository.updateChapters( - chapterIds = chapters.map { it.id }, - bookmarked = bookmarked, - read = read, - lastPageRead = lastPageRead, - ).onEach { serverListeners.updateChapters(chapters.map { it.id }) } - - companion object { - private val log = logging() +@Inject +class UpdateChapter( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + chapterId: Long, + bookmarked: Boolean? = null, + read: Boolean? = null, + lastPageRead: Int? = null, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapterId, bookmarked, read, lastPageRead) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter bookmark for chapter $chapterId" } } + .collect() + + suspend fun await( + chapter: Chapter, + bookmarked: Boolean? = null, + read: Boolean? = null, + lastPageRead: Int? = null, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter, bookmarked, read, lastPageRead) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter bookmark for chapter ${chapter.index} of ${chapter.mangaId}" } + } + .collect() + + suspend fun await( + chapterIds: List, + bookmarked: Boolean? = null, + read: Boolean? = null, + lastPageRead: Int? = null, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapterIds, bookmarked, read, lastPageRead) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter bookmark for chapters $chapterIds" } + } + .collect() + + @JvmName("awaitChapters") + suspend fun await( + chapters: List, + bookmarked: Boolean? = null, + read: Boolean? = null, + lastPageRead: Int? = null, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapters, bookmarked, read, lastPageRead) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter bookmark for chapters ${chapters.joinToString { it.id.toString() }}" } + } + .collect() + + fun asFlow( + chapterId: Long, + bookmarked: Boolean? = null, + read: Boolean? = null, + lastPageRead: Int? = null, + ) = chapterRepository.updateChapter( + chapterId = chapterId, + bookmarked = bookmarked, + read = read, + lastPageRead = lastPageRead, + ).onEach { serverListeners.updateChapters(chapterId) } + + fun asFlow( + chapter: Chapter, + bookmarked: Boolean? = null, + read: Boolean? = null, + lastPageRead: Int? = null, + ) = chapterRepository.updateChapter( + chapterId = chapter.id, + bookmarked = bookmarked, + read = read, + lastPageRead = lastPageRead, + ).onEach { serverListeners.updateChapters(chapter.id) } + + fun asFlow( + chapterIds: List, + bookmarked: Boolean? = null, + read: Boolean? = null, + lastPageRead: Int? = null, + ) = chapterRepository.updateChapters( + chapterIds = chapterIds, + bookmarked = bookmarked, + read = read, + lastPageRead = lastPageRead, + ).onEach { serverListeners.updateChapters(chapterIds) } + + @JvmName("asFlowChapters") + fun asFlow( + chapters: List, + bookmarked: Boolean? = null, + read: Boolean? = null, + lastPageRead: Int? = null, + ) = chapterRepository.updateChapters( + chapterIds = chapters.map { it.id }, + bookmarked = bookmarked, + read = read, + lastPageRead = lastPageRead, + ).onEach { serverListeners.updateChapters(chapters.map { it.id }) } + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterLastPageRead.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterLastPageRead.kt index 2ba09753..bbf4da26 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterLastPageRead.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterLastPageRead.kt @@ -15,51 +15,50 @@ import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateChapterLastPageRead - @Inject - constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - chapterId: Long, - lastPageRead: Int, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapterId, lastPageRead) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter last page read for chapter $chapterId" } - } - .collect() - - suspend fun await( - chapter: Chapter, - lastPageRead: Int, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapter, lastPageRead) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter last page read for chapter ${chapter.index} of ${chapter.mangaId}" } - } - .collect() - - fun asFlow( - chapterId: Long, - lastPageRead: Int, - ) = chapterRepository.updateChapter( - chapterId = chapterId, - lastPageRead = lastPageRead, - ).onEach { serverListeners.updateChapters(chapterId) } - - fun asFlow( - chapter: Chapter, - lastPageRead: Int, - ) = chapterRepository.updateChapter( - chapterId = chapter.id, - lastPageRead = lastPageRead, - ).onEach { serverListeners.updateChapters(chapter.id) } - - companion object { - private val log = logging() +@Inject +class UpdateChapterLastPageRead( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + chapterId: Long, + lastPageRead: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapterId, lastPageRead) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter last page read for chapter $chapterId" } } + .collect() + + suspend fun await( + chapter: Chapter, + lastPageRead: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter, lastPageRead) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter last page read for chapter ${chapter.index} of ${chapter.mangaId}" } + } + .collect() + + fun asFlow( + chapterId: Long, + lastPageRead: Int, + ) = chapterRepository.updateChapter( + chapterId = chapterId, + lastPageRead = lastPageRead, + ).onEach { serverListeners.updateChapters(chapterId) } + + fun asFlow( + chapter: Chapter, + lastPageRead: Int, + ) = chapterRepository.updateChapter( + chapterId = chapter.id, + lastPageRead = lastPageRead, + ).onEach { serverListeners.updateChapters(chapter.id) } + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterMeta.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterMeta.kt index 62da46ae..bb06c8a1 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterMeta.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterMeta.kt @@ -15,39 +15,38 @@ import kotlinx.coroutines.flow.flow import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateChapterMeta - @Inject - constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - chapter: Chapter, - pageOffset: Int = chapter.meta.juiPageOffset, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapter, pageOffset) - .catch { - onError(it) - log.warn(it) { "Failed to update ${chapter.name}(${chapter.index}) meta" } - } - .collect() - - fun asFlow( - chapter: Chapter, - pageOffset: Int = chapter.meta.juiPageOffset, - ) = flow { - if (pageOffset != chapter.meta.juiPageOffset) { - chapterRepository.updateChapterMeta( - chapter.id, - "juiPageOffset", - pageOffset.toString(), - ).collect() - serverListeners.updateChapters(chapter.id) - } - emit(Unit) +@Inject +class UpdateChapterMeta( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + chapter: Chapter, + pageOffset: Int = chapter.meta.juiPageOffset, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter, pageOffset) + .catch { + onError(it) + log.warn(it) { "Failed to update ${chapter.name}(${chapter.index}) meta" } } + .collect() - companion object { - private val log = logging() + fun asFlow( + chapter: Chapter, + pageOffset: Int = chapter.meta.juiPageOffset, + ) = flow { + if (pageOffset != chapter.meta.juiPageOffset) { + chapterRepository.updateChapterMeta( + chapter.id, + "juiPageOffset", + pageOffset.toString(), + ).collect() + serverListeners.updateChapters(chapter.id) } + emit(Unit) } + + companion object { + private val log = logging() + } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterRead.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterRead.kt index c42faad8..8cac7444 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterRead.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterRead.kt @@ -15,59 +15,58 @@ import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateChapterRead - @Inject - constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - chapterId: Long, - read: Boolean, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapterId, read) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter read status for chapter $chapterId" } - } - .collect() - - suspend fun await( - chapter: Chapter, - read: Boolean, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapter, read) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter read status for chapter ${chapter.index} of ${chapter.mangaId}" } - } - .collect() - - fun asFlow( - chapterId: Long, - read: Boolean, - ) = chapterRepository.updateChapter( - chapterId = chapterId, - read = read, - ).onEach { serverListeners.updateChapters(chapterId) } - - fun asFlow( - chapterIds: List, - read: Boolean, - ) = chapterRepository.updateChapters( - chapterIds = chapterIds, - read = read, - ).onEach { serverListeners.updateChapters(chapterIds) } - - fun asFlow( - chapter: Chapter, - read: Boolean, - ) = chapterRepository.updateChapter( - chapterId = chapter.id, - read = read, - ).onEach { serverListeners.updateChapters(chapter.id) } - - companion object { - private val log = logging() +@Inject +class UpdateChapterRead( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + chapterId: Long, + read: Boolean, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapterId, read) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter read status for chapter $chapterId" } } + .collect() + + suspend fun await( + chapter: Chapter, + read: Boolean, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter, read) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter read status for chapter ${chapter.index} of ${chapter.mangaId}" } + } + .collect() + + fun asFlow( + chapterId: Long, + read: Boolean, + ) = chapterRepository.updateChapter( + chapterId = chapterId, + read = read, + ).onEach { serverListeners.updateChapters(chapterId) } + + fun asFlow( + chapterIds: List, + read: Boolean, + ) = chapterRepository.updateChapters( + chapterIds = chapterIds, + read = read, + ).onEach { serverListeners.updateChapters(chapterIds) } + + fun asFlow( + chapter: Chapter, + read: Boolean, + ) = chapterRepository.updateChapter( + chapterId = chapter.id, + read = read, + ).onEach { serverListeners.updateChapters(chapter.id) } + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/BatchChapterDownload.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/BatchChapterDownload.kt index 6ebbfe41..64e9c9b9 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/BatchChapterDownload.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/BatchChapterDownload.kt @@ -12,36 +12,35 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class BatchChapterDownload - @Inject - constructor( - private val downloadRepository: DownloadRepository, - ) { - suspend fun await( - chapterIds: List, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapterIds) - .catch { - onError(it) - log.warn(it) { "Failed to queue chapters $chapterIds for a download" } - } - .collect() - - suspend fun await( - vararg chapterIds: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(*chapterIds) - .catch { - onError(it) - log.warn(it) { "Failed to queue chapters ${chapterIds.asList()} for a download" } - } - .collect() - - fun asFlow(chapterIds: List) = downloadRepository.batchDownload(chapterIds) - - fun asFlow(vararg chapterIds: Long) = downloadRepository.batchDownload(chapterIds.asList()) - - companion object { - private val log = logging() +@Inject +class BatchChapterDownload( + private val downloadRepository: DownloadRepository, +) { + suspend fun await( + chapterIds: List, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapterIds) + .catch { + onError(it) + log.warn(it) { "Failed to queue chapters $chapterIds for a download" } } + .collect() + + suspend fun await( + vararg chapterIds: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(*chapterIds) + .catch { + onError(it) + log.warn(it) { "Failed to queue chapters ${chapterIds.asList()} for a download" } + } + .collect() + + fun asFlow(chapterIds: List) = downloadRepository.batchDownload(chapterIds) + + fun asFlow(vararg chapterIds: Long) = downloadRepository.batchDownload(chapterIds.asList()) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ClearDownloadQueue.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ClearDownloadQueue.kt index 28352181..932761d6 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ClearDownloadQueue.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ClearDownloadQueue.kt @@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class ClearDownloadQueue - @Inject - constructor( - private val downloadRepository: DownloadRepository, - ) { - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = - asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to clear download queue" } - } - .collect() +@Inject +class ClearDownloadQueue( + private val downloadRepository: DownloadRepository, +) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to clear download queue" } + } + .collect() - fun asFlow() = downloadRepository.clearDownloadQueue() + fun asFlow() = downloadRepository.clearDownloadQueue() - companion object { - private val log = logging() - } + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/QueueChapterDownload.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/QueueChapterDownload.kt index cfcd1b3a..dc1bd53a 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/QueueChapterDownload.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/QueueChapterDownload.kt @@ -12,24 +12,23 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class QueueChapterDownload - @Inject - constructor( - private val downloadRepository: DownloadRepository, - ) { - suspend fun await( - chapterId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapterId) - .catch { - onError(it) - log.warn(it) { "Failed to queue chapter $chapterId for a download" } - } - .collect() - - fun asFlow(chapterId: Long) = downloadRepository.queueChapterDownload(chapterId) - - companion object { - private val log = logging() +@Inject +class QueueChapterDownload( + private val downloadRepository: DownloadRepository, +) { + suspend fun await( + chapterId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapterId) + .catch { + onError(it) + log.warn(it) { "Failed to queue chapter $chapterId for a download" } } + .collect() + + fun asFlow(chapterId: Long) = downloadRepository.queueChapterDownload(chapterId) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ReorderChapterDownload.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ReorderChapterDownload.kt index eba80cd1..257e22e3 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ReorderChapterDownload.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ReorderChapterDownload.kt @@ -12,28 +12,27 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class ReorderChapterDownload - @Inject - constructor( - private val downloadRepository: DownloadRepository, - ) { - suspend fun await( - chapterId: Long, - to: Int, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapterId, to) - .catch { - onError(it) - log.warn(it) { "Failed to reorder chapter download for $chapterId to $to" } - } - .collect() - - fun asFlow( - chapterId: Long, - to: Int, - ) = downloadRepository.reorderChapterDownload(chapterId, to) - - companion object { - private val log = logging() +@Inject +class ReorderChapterDownload( + private val downloadRepository: DownloadRepository, +) { + suspend fun await( + chapterId: Long, + to: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapterId, to) + .catch { + onError(it) + log.warn(it) { "Failed to reorder chapter download for $chapterId to $to" } } + .collect() + + fun asFlow( + chapterId: Long, + to: Int, + ) = downloadRepository.reorderChapterDownload(chapterId, to) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StartDownloading.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StartDownloading.kt index c15e3a3f..e8756a70 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StartDownloading.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StartDownloading.kt @@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class StartDownloading - @Inject - constructor( - private val downloadRepository: DownloadRepository, - ) { - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = - asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to start downloader" } - } - .collect() +@Inject +class StartDownloading( + private val downloadRepository: DownloadRepository, +) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to start downloader" } + } + .collect() - fun asFlow() = downloadRepository.startDownloading() + fun asFlow() = downloadRepository.startDownloading() - companion object { - private val log = logging() - } + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopChapterDownload.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopChapterDownload.kt index 22ca2515..711f343d 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopChapterDownload.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopChapterDownload.kt @@ -12,24 +12,23 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class StopChapterDownload - @Inject - constructor( - private val downloadRepository: DownloadRepository, - ) { - suspend fun await( - chapterId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapterId) - .catch { - onError(it) - log.warn(it) { "Failed to stop chapter download for $chapterId" } - } - .collect() - - fun asFlow(chapterId: Long) = downloadRepository.stopChapterDownload(chapterId) - - companion object { - private val log = logging() +@Inject +class StopChapterDownload( + private val downloadRepository: DownloadRepository, +) { + suspend fun await( + chapterId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapterId) + .catch { + onError(it) + log.warn(it) { "Failed to stop chapter download for $chapterId" } } + .collect() + + fun asFlow(chapterId: Long) = downloadRepository.stopChapterDownload(chapterId) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopDownloading.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopDownloading.kt index a43e8769..41f60184 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopDownloading.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopDownloading.kt @@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class StopDownloading - @Inject - constructor( - private val downloadRepository: DownloadRepository, - ) { - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = - asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to stop downloader" } - } - .collect() +@Inject +class StopDownloading( + private val downloadRepository: DownloadRepository, +) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to stop downloader" } + } + .collect() - fun asFlow() = downloadRepository.stopDownloading() + fun asFlow() = downloadRepository.stopDownloading() - companion object { - private val log = logging() - } + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadService.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadService.kt index 1371890c..87398cea 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadService.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadService.kt @@ -16,42 +16,40 @@ import io.ktor.websocket.Frame import io.ktor.websocket.readText import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map -import kotlinx.serialization.decodeFromString import me.tatarka.inject.annotations.Inject -class DownloadService - @Inject - constructor( - serverPreferences: ServerPreferences, - client: Http, - ) : WebsocketService(serverPreferences, client) { - override val _status: MutableStateFlow - get() = status +@Inject +class DownloadService( + serverPreferences: ServerPreferences, + client: Http, +) : WebsocketService(serverPreferences, client) { + override val _status: MutableStateFlow + get() = status - override val query: String - get() = "/api/v1/downloads" + override val query: String + get() = "/api/v1/downloads" - override suspend fun onReceived(frame: Frame.Text) { - val status = json.decodeFromString(frame.readText()) - downloaderStatus.value = status.status - downloadQueue.value = status.queue - } - - companion object { - val status = MutableStateFlow(Status.STARTING) - val downloadQueue = MutableStateFlow(emptyList()) - val downloaderStatus = MutableStateFlow(DownloaderStatus.Stopped) - - fun registerWatch(mangaId: Long) = - downloadQueue - .map { - it.filter { it.mangaId == mangaId } - } - - fun registerWatches(mangaIds: Set) = - downloadQueue - .map { - it.filter { it.mangaId in mangaIds } - } - } + override suspend fun onReceived(frame: Frame.Text) { + val status = json.decodeFromString(frame.readText()) + downloaderStatus.value = status.status + downloadQueue.value = status.queue } + + companion object { + val status = MutableStateFlow(Status.STARTING) + val downloadQueue = MutableStateFlow(emptyList()) + val downloaderStatus = MutableStateFlow(DownloaderStatus.Stopped) + + fun registerWatch(mangaId: Long) = + downloadQueue + .map { + it.filter { it.mangaId == mangaId } + } + + fun registerWatches(mangaIds: Set) = + downloadQueue + .map { + it.filter { it.mangaId in mangaIds } + } + } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/GetExtensionList.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/GetExtensionList.kt index c1f535eb..133e3e6e 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/GetExtensionList.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/GetExtensionList.kt @@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetExtensionList - @Inject - constructor( - private val extensionRepository: ExtensionRepository, - ) { - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = - asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to get extension list" } - } - .singleOrNull() +@Inject +class GetExtensionList( + private val extensionRepository: ExtensionRepository, +) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to get extension list" } + } + .singleOrNull() - fun asFlow() = extensionRepository.getExtensionList() + fun asFlow() = extensionRepository.getExtensionList() - companion object { - private val log = logging() - } + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtension.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtension.kt index cb157c18..122ed2be 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtension.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtension.kt @@ -13,24 +13,23 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class InstallExtension - @Inject - constructor( - private val extensionRepository: ExtensionRepository, - ) { - suspend fun await( - extension: Extension, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(extension) - .catch { - onError(it) - log.warn(it) { "Failed to install extension ${extension.apkName}" } - } - .collect() - - fun asFlow(extension: Extension) = extensionRepository.installExtension(extension.pkgName) - - companion object { - private val log = logging() +@Inject +class InstallExtension( + private val extensionRepository: ExtensionRepository, +) { + suspend fun await( + extension: Extension, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(extension) + .catch { + onError(it) + log.warn(it) { "Failed to install extension ${extension.apkName}" } } + .collect() + + fun asFlow(extension: Extension) = extensionRepository.installExtension(extension.pkgName) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtensionFile.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtensionFile.kt index f25bc0d6..68f6767f 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtensionFile.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtensionFile.kt @@ -15,24 +15,23 @@ import okio.Path import okio.SYSTEM import org.lighthousegames.logging.logging -class InstallExtensionFile - @Inject - constructor( - private val extensionRepository: ExtensionRepository, - ) { - suspend fun await( - path: Path, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(path) - .catch { - onError(it) - log.warn(it) { "Failed to install extension from $path" } - } - .collect() - - fun asFlow(path: Path) = extensionRepository.installExtension(FileSystem.SYSTEM.source(path)) - - companion object { - private val log = logging() +@Inject +class InstallExtensionFile( + private val extensionRepository: ExtensionRepository, +) { + suspend fun await( + path: Path, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(path) + .catch { + onError(it) + log.warn(it) { "Failed to install extension from $path" } } + .collect() + + fun asFlow(path: Path) = extensionRepository.installExtension(FileSystem.SYSTEM.source(path)) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UninstallExtension.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UninstallExtension.kt index 1a84e122..88257e0a 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UninstallExtension.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UninstallExtension.kt @@ -13,24 +13,23 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UninstallExtension - @Inject - constructor( - private val extensionRepository: ExtensionRepository, - ) { - suspend fun await( - extension: Extension, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(extension) - .catch { - onError(it) - log.warn(it) { "Failed to uninstall extension ${extension.apkName}" } - } - .collect() - - fun asFlow(extension: Extension) = extensionRepository.uninstallExtension(extension.pkgName) - - companion object { - private val log = logging() +@Inject +class UninstallExtension( + private val extensionRepository: ExtensionRepository, +) { + suspend fun await( + extension: Extension, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(extension) + .catch { + onError(it) + log.warn(it) { "Failed to uninstall extension ${extension.apkName}" } } + .collect() + + fun asFlow(extension: Extension) = extensionRepository.uninstallExtension(extension.pkgName) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UpdateExtension.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UpdateExtension.kt index 8e4a9821..d842712e 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UpdateExtension.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UpdateExtension.kt @@ -13,24 +13,23 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateExtension - @Inject - constructor( - private val extensionRepository: ExtensionRepository, - ) { - suspend fun await( - extension: Extension, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(extension) - .catch { - onError(it) - log.warn(it) { "Failed to update extension ${extension.apkName}" } - } - .collect() - - fun asFlow(extension: Extension) = extensionRepository.updateExtension(extension.pkgName) - - companion object { - private val log = logging() +@Inject +class UpdateExtension( + private val extensionRepository: ExtensionRepository, +) { + suspend fun await( + extension: Extension, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(extension) + .catch { + onError(it) + log.warn(it) { "Failed to update extension ${extension.apkName}" } } + .collect() + + fun asFlow(extension: Extension) = extensionRepository.updateExtension(extension.pkgName) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/GetGlobalMeta.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/GetGlobalMeta.kt index 1a43770a..b43eda06 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/GetGlobalMeta.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/GetGlobalMeta.kt @@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetGlobalMeta - @Inject - constructor( - private val globalRepository: GlobalRepository, - ) { - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = - asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to get global meta" } - } - .singleOrNull() +@Inject +class GetGlobalMeta( + private val globalRepository: GlobalRepository, +) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to get global meta" } + } + .singleOrNull() - fun asFlow() = globalRepository.getGlobalMeta() + fun asFlow() = globalRepository.getGlobalMeta() - companion object { - private val log = logging() - } + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/UpdateGlobalMeta.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/UpdateGlobalMeta.kt index 7f67d1fa..b940376b 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/UpdateGlobalMeta.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/UpdateGlobalMeta.kt @@ -14,36 +14,35 @@ import kotlinx.coroutines.flow.flow import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateGlobalMeta - @Inject - constructor( - private val globalRepository: GlobalRepository, - ) { - suspend fun await( - globalMeta: GlobalMeta, - example: Int = globalMeta.example, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(globalMeta, example) - .catch { - onError(it) - log.warn(it) { "Failed to update global meta" } - } - .collect() - - fun asFlow( - globalMeta: GlobalMeta, - example: Int = globalMeta.example, - ) = flow { - if (example != globalMeta.example) { - globalRepository.updateGlobalMeta( - "example", - example.toString(), - ).collect() - } - emit(Unit) +@Inject +class UpdateGlobalMeta( + private val globalRepository: GlobalRepository, +) { + suspend fun await( + globalMeta: GlobalMeta, + example: Int = globalMeta.example, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(globalMeta, example) + .catch { + onError(it) + log.warn(it) { "Failed to update global meta" } } + .collect() - companion object { - private val log = logging() + fun asFlow( + globalMeta: GlobalMeta, + example: Int = globalMeta.example, + ) = flow { + if (example != globalMeta.example) { + globalRepository.updateGlobalMeta( + "example", + example.toString(), + ).collect() } + emit(Unit) } + + companion object { + private val log = logging() + } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/AddMangaToLibrary.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/AddMangaToLibrary.kt index dab8756f..80460dec 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/AddMangaToLibrary.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/AddMangaToLibrary.kt @@ -15,41 +15,40 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class AddMangaToLibrary - @Inject - constructor( - private val libraryRepository: LibraryRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - mangaId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId) - .catch { - onError(it) - log.warn(it) { "Failed to add $mangaId to library" } - } - .singleOrNull() - - suspend fun await( - manga: Manga, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga) - .catch { - onError(it) - log.warn(it) { "Failed to add ${manga.title}(${manga.id}) to library" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = - libraryRepository.addMangaToLibrary(mangaId) - .onEach { serverListeners.updateManga(mangaId) } - - fun asFlow(manga: Manga) = - libraryRepository.addMangaToLibrary(manga.id) - .onEach { serverListeners.updateManga(manga.id) } - - companion object { - private val log = logging() +@Inject +class AddMangaToLibrary( + private val libraryRepository: LibraryRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .catch { + onError(it) + log.warn(it) { "Failed to add $mangaId to library" } } + .singleOrNull() + + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .catch { + onError(it) + log.warn(it) { "Failed to add ${manga.title}(${manga.id}) to library" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = + libraryRepository.addMangaToLibrary(mangaId) + .onEach { serverListeners.updateManga(mangaId) } + + fun asFlow(manga: Manga) = + libraryRepository.addMangaToLibrary(manga.id) + .onEach { serverListeners.updateManga(manga.id) } + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/RemoveMangaFromLibrary.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/RemoveMangaFromLibrary.kt index 7f68749d..1e860366 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/RemoveMangaFromLibrary.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/RemoveMangaFromLibrary.kt @@ -15,41 +15,40 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class RemoveMangaFromLibrary - @Inject - constructor( - private val libraryRepository: LibraryRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - mangaId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId) - .catch { - onError(it) - log.warn(it) { "Failed to remove $mangaId from library" } - } - .singleOrNull() - - suspend fun await( - manga: Manga, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga) - .catch { - onError(it) - log.warn(it) { "Failed to remove ${manga.title}(${manga.id}) from library" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = - libraryRepository.removeMangaFromLibrary(mangaId) - .onEach { serverListeners.updateManga(mangaId) } - - fun asFlow(manga: Manga) = - libraryRepository.removeMangaFromLibrary(manga.id) - .onEach { serverListeners.updateManga(manga.id) } - - companion object { - private val log = logging() +@Inject +class RemoveMangaFromLibrary( + private val libraryRepository: LibraryRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .catch { + onError(it) + log.warn(it) { "Failed to remove $mangaId from library" } } + .singleOrNull() + + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .catch { + onError(it) + log.warn(it) { "Failed to remove ${manga.title}(${manga.id}) from library" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = + libraryRepository.removeMangaFromLibrary(mangaId) + .onEach { serverListeners.updateManga(mangaId) } + + fun asFlow(manga: Manga) = + libraryRepository.removeMangaFromLibrary(manga.id) + .onEach { serverListeners.updateManga(manga.id) } + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryUpdateService.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryUpdateService.kt index 00934243..096672ba 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryUpdateService.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryUpdateService.kt @@ -16,26 +16,25 @@ import kotlinx.coroutines.flow.MutableStateFlow import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class LibraryUpdateService - @Inject - constructor( - serverPreferences: ServerPreferences, - client: Http, - ) : WebsocketService(serverPreferences, client) { - override val _status: MutableStateFlow - get() = status +@Inject +class LibraryUpdateService( + serverPreferences: ServerPreferences, + client: Http, +) : WebsocketService(serverPreferences, client) { + override val _status: MutableStateFlow + get() = status - override val query: String - get() = "/api/v1/update" + override val query: String + get() = "/api/v1/update" - override suspend fun onReceived(frame: Frame.Text) { - updateStatus.value = json.decodeFromString(frame.readText()) - } - - companion object { - private val log = logging() - - val status = MutableStateFlow(Status.STARTING) - val updateStatus = MutableStateFlow(UpdateStatus(emptyMap(), emptyMap(), false)) - } + override suspend fun onReceived(frame: Frame.Text) { + updateStatus.value = json.decodeFromString(frame.readText()) } + + companion object { + private val log = logging() + + val status = MutableStateFlow(Status.STARTING) + val updateStatus = MutableStateFlow(UpdateStatus(emptyMap(), emptyMap(), false)) + } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetManga.kt index c3a93b38..d1d5e617 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetManga.kt @@ -15,45 +15,44 @@ import kotlinx.coroutines.flow.take import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetManga - @Inject - constructor( - private val mangaRepository: MangaRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - mangaId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get manga $mangaId" } - } - .singleOrNull() - - suspend fun await( - manga: Manga, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get manga ${manga.title}(${manga.id})" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = - serverListeners.combineMangaUpdates( - mangaRepository.getManga(mangaId), - ) { mangaId in it } - - fun asFlow(manga: Manga) = - serverListeners.combineMangaUpdates( - mangaRepository.getManga(manga.id), - ) { manga.id in it } - - companion object { - private val log = logging() +@Inject +class GetManga( + private val mangaRepository: MangaRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get manga $mangaId" } } + .singleOrNull() + + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get manga ${manga.title}(${manga.id})" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = + serverListeners.combineMangaUpdates( + mangaRepository.getManga(mangaId), + ) { mangaId in it } + + fun asFlow(manga: Manga) = + serverListeners.combineMangaUpdates( + mangaRepository.getManga(manga.id), + ) { manga.id in it } + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshManga.kt index 26911fb1..6110961e 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshManga.kt @@ -16,39 +16,38 @@ import kotlinx.coroutines.flow.take import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class RefreshManga - @Inject - constructor( - private val mangaRepository: MangaRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - mangaId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to refresh manga $mangaId" } - } - .singleOrNull() - - suspend fun await( - manga: Manga, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to refresh manga ${manga.title}(${manga.id})" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = mangaRepository.refreshManga(mangaId).onEach { serverListeners.updateManga(mangaId) } - - fun asFlow(manga: Manga) = mangaRepository.refreshManga(manga.id).onEach { serverListeners.updateManga(manga.id) } - - companion object { - private val log = logging() +@Inject +class RefreshManga( + private val mangaRepository: MangaRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to refresh manga $mangaId" } } + .singleOrNull() + + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to refresh manga ${manga.title}(${manga.id})" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = mangaRepository.refreshManga(mangaId).onEach { serverListeners.updateManga(mangaId) } + + fun asFlow(manga: Manga) = mangaRepository.refreshManga(manga.id).onEach { serverListeners.updateManga(manga.id) } + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/UpdateMangaMeta.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/UpdateMangaMeta.kt index 5f6af7f7..a66d1fd9 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/UpdateMangaMeta.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/UpdateMangaMeta.kt @@ -17,39 +17,38 @@ import kotlinx.coroutines.flow.flow import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateMangaMeta - @Inject - constructor( - private val mangaRepository: MangaRepository, - private val serverListeners: ServerListeners, - ) { - suspend fun await( - manga: Manga, - readerMode: String = manga.meta.juiReaderMode, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga, readerMode) - .catch { - onError(it) - log.warn(it) { "Failed to update ${manga.title}(${manga.id}) meta" } - } - .collect() - - fun asFlow( - manga: Manga, - readerMode: String = manga.meta.juiReaderMode.decodeURLQueryComponent(), - ) = flow { - if (readerMode.encodeURLQueryComponent() != manga.meta.juiReaderMode) { - mangaRepository.updateMangaMeta( - manga.id, - "juiReaderMode", - readerMode, - ).collect() - serverListeners.updateManga(manga.id) - } - emit(Unit) +@Inject +class UpdateMangaMeta( + private val mangaRepository: MangaRepository, + private val serverListeners: ServerListeners, +) { + suspend fun await( + manga: Manga, + readerMode: String = manga.meta.juiReaderMode, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, readerMode) + .catch { + onError(it) + log.warn(it) { "Failed to update ${manga.title}(${manga.id}) meta" } } + .collect() - companion object { - private val log = logging() + fun asFlow( + manga: Manga, + readerMode: String = manga.meta.juiReaderMode.decodeURLQueryComponent(), + ) = flow { + if (readerMode.encodeURLQueryComponent() != manga.meta.juiReaderMode) { + mangaRepository.updateMangaMeta( + manga.id, + "juiReaderMode", + readerMode, + ).collect() + serverListeners.updateManga(manga.id) } + emit(Unit) } + + companion object { + private val log = logging() + } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/migration/interactor/RunMigrations.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/migration/interactor/RunMigrations.kt index be43add6..e84ba057 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/migration/interactor/RunMigrations.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/migration/interactor/RunMigrations.kt @@ -11,20 +11,19 @@ import ca.gosyer.jui.domain.migration.service.MigrationPreferences import ca.gosyer.jui.domain.reader.service.ReaderPreferences import me.tatarka.inject.annotations.Inject -class RunMigrations - @Inject - constructor( - private val migrationPreferences: MigrationPreferences, - private val readerPreferences: ReaderPreferences, - ) { - fun runMigrations() { - val code = migrationPreferences.version().get() - if (code <= 0) { - readerPreferences.modes().get().forEach { - readerPreferences.getMode(it).direction().delete() - } - migrationPreferences.version().set(BuildKonfig.MIGRATION_CODE) - return +@Inject +class RunMigrations( + private val migrationPreferences: MigrationPreferences, + private val readerPreferences: ReaderPreferences, +) { + fun runMigrations() { + val code = migrationPreferences.version().get() + if (code <= 0) { + readerPreferences.modes().get().forEach { + readerPreferences.getMode(it).direction().delete() } + migrationPreferences.version().set(BuildKonfig.MIGRATION_CODE) + return } } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/AboutServer.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/AboutServer.kt index 5e3dc412..0f9c5e0b 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/AboutServer.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/AboutServer.kt @@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class AboutServer - @Inject - constructor( - private val settingsRepository: SettingsRepository, - ) { - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = - asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to get server information" } - } - .singleOrNull() +@Inject +class AboutServer( + private val settingsRepository: SettingsRepository, +) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to get server information" } + } + .singleOrNull() - fun asFlow() = settingsRepository.aboutServer() + fun asFlow() = settingsRepository.aboutServer() - companion object { - private val log = logging() - } + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/GetSettings.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/GetSettings.kt index 460b2974..196a1060 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/GetSettings.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/GetSettings.kt @@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetSettings - @Inject - constructor( - private val settingsRepository: SettingsRepository, - ) { - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = - asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to check for server updates" } - } - .singleOrNull() +@Inject +class GetSettings( + private val settingsRepository: SettingsRepository, +) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to check for server updates" } + } + .singleOrNull() - fun asFlow() = settingsRepository.getSettings() + fun asFlow() = settingsRepository.getSettings() - companion object { - private val log = logging() - } + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/SetSettings.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/SetSettings.kt index 450ce1c8..5338c92e 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/SetSettings.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/SetSettings.kt @@ -13,24 +13,23 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class SetSettings - @Inject - constructor( - private val settingsRepository: SettingsRepository, - ) { - suspend fun await( - input: SetSettingsInput, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(input) - .catch { - onError(it) - log.warn(it) { "Failed to check for server updates" } - } - .singleOrNull() - - fun asFlow(input: SetSettingsInput) = settingsRepository.setSettings(input) - - companion object { - private val log = logging() +@Inject +class SetSettings( + private val settingsRepository: SettingsRepository, +) { + suspend fun await( + input: SetSettingsInput, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(input) + .catch { + onError(it) + log.warn(it) { "Failed to check for server updates" } } + .singleOrNull() + + fun asFlow(input: SetSettingsInput) = settingsRepository.setSettings(input) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetFilterList.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetFilterList.kt index 4c6375df..5250d72e 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetFilterList.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetFilterList.kt @@ -13,36 +13,35 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetFilterList - @Inject - constructor( - private val sourceRepository: SourceRepository, - ) { - suspend fun await( - source: Source, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(source.id) - .catch { - onError(it) - log.warn(it) { "Failed to get filter list for ${source.displayName}" } - } - .singleOrNull() - - suspend fun await( - sourceId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(sourceId) - .catch { - onError(it) - log.warn(it) { "Failed to get filter list for $sourceId" } - } - .singleOrNull() - - fun asFlow(source: Source) = sourceRepository.getFilterList(source.id) - - fun asFlow(sourceId: Long) = sourceRepository.getFilterList(sourceId) - - companion object { - private val log = logging() +@Inject +class GetFilterList( + private val sourceRepository: SourceRepository, +) { + suspend fun await( + source: Source, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(source.id) + .catch { + onError(it) + log.warn(it) { "Failed to get filter list for ${source.displayName}" } } + .singleOrNull() + + suspend fun await( + sourceId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(sourceId) + .catch { + onError(it) + log.warn(it) { "Failed to get filter list for $sourceId" } + } + .singleOrNull() + + fun asFlow(source: Source) = sourceRepository.getFilterList(source.id) + + fun asFlow(sourceId: Long) = sourceRepository.getFilterList(sourceId) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetLatestManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetLatestManga.kt index a5cabf65..fc46420a 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetLatestManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetLatestManga.kt @@ -13,44 +13,43 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetLatestManga - @Inject - constructor( - private val sourceRepository: SourceRepository, - ) { - suspend fun await( - source: Source, - page: Int, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(source.id, page) - .catch { - onError(it) - log.warn(it) { "Failed to get latest manga from ${source.displayName} on page $page" } - } - .singleOrNull() - - suspend fun await( - sourceId: Long, - page: Int, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(sourceId, page) - .catch { - onError(it) - log.warn(it) { "Failed to get latest manga from $sourceId on page $page" } - } - .singleOrNull() - - fun asFlow( - source: Source, - page: Int, - ) = sourceRepository.getLatestManga(source.id, page) - - fun asFlow( - sourceId: Long, - page: Int, - ) = sourceRepository.getLatestManga(sourceId, page) - - companion object { - private val log = logging() +@Inject +class GetLatestManga( + private val sourceRepository: SourceRepository, +) { + suspend fun await( + source: Source, + page: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(source.id, page) + .catch { + onError(it) + log.warn(it) { "Failed to get latest manga from ${source.displayName} on page $page" } } + .singleOrNull() + + suspend fun await( + sourceId: Long, + page: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(sourceId, page) + .catch { + onError(it) + log.warn(it) { "Failed to get latest manga from $sourceId on page $page" } + } + .singleOrNull() + + fun asFlow( + source: Source, + page: Int, + ) = sourceRepository.getLatestManga(source.id, page) + + fun asFlow( + sourceId: Long, + page: Int, + ) = sourceRepository.getLatestManga(sourceId, page) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetPopularManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetPopularManga.kt index 2fd6b75f..b5e971d6 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetPopularManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetPopularManga.kt @@ -13,44 +13,43 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetPopularManga - @Inject - constructor( - private val sourceRepository: SourceRepository, - ) { - suspend fun await( - source: Source, - page: Int, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(source.id, page) - .catch { - onError(it) - log.warn(it) { "Failed to get popular manga from ${source.displayName} on page $page" } - } - .singleOrNull() - - suspend fun await( - sourceId: Long, - page: Int, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(sourceId, page) - .catch { - onError(it) - log.warn(it) { "Failed to get popular manga from $sourceId on page $page" } - } - .singleOrNull() - - fun asFlow( - source: Source, - page: Int, - ) = sourceRepository.getPopularManga(source.id, page) - - fun asFlow( - sourceId: Long, - page: Int, - ) = sourceRepository.getPopularManga(sourceId, page) - - companion object { - private val log = logging() +@Inject +class GetPopularManga( + private val sourceRepository: SourceRepository, +) { + suspend fun await( + source: Source, + page: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(source.id, page) + .catch { + onError(it) + log.warn(it) { "Failed to get popular manga from ${source.displayName} on page $page" } } + .singleOrNull() + + suspend fun await( + sourceId: Long, + page: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(sourceId, page) + .catch { + onError(it) + log.warn(it) { "Failed to get popular manga from $sourceId on page $page" } + } + .singleOrNull() + + fun asFlow( + source: Source, + page: Int, + ) = sourceRepository.getPopularManga(source.id, page) + + fun asFlow( + sourceId: Long, + page: Int, + ) = sourceRepository.getPopularManga(sourceId, page) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSearchManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSearchManga.kt index 9579957a..3c3a053e 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSearchManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSearchManga.kt @@ -14,64 +14,63 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetSearchManga - @Inject - constructor( - private val sourceRepository: SourceRepository, - ) { - suspend fun await( - source: Source, - page: Int, - searchTerm: String?, - filters: List?, - onError: suspend (Throwable) -> Unit = { - }, - ) = asFlow(source.id, page, searchTerm, filters) - .catch { - onError(it) - log.warn(it) { "Failed to get search results from ${source.displayName} on page $page with query '$searchTerm'" } - } - .singleOrNull() - - suspend fun await( - sourceId: Long, - searchTerm: String?, - page: Int, - filters: List?, - onError: suspend (Throwable) -> Unit = { - }, - ) = asFlow(sourceId, page, searchTerm, filters) - .catch { - onError(it) - log.warn(it) { "Failed to get search results from $sourceId on page $page with query '$searchTerm'" } - } - .singleOrNull() - - fun asFlow( - source: Source, - page: Int, - searchTerm: String?, - filters: List?, - ) = sourceRepository.getSearchResults( - source.id, - page, - searchTerm?.ifBlank { null }, - filters, - ) - - fun asFlow( - sourceId: Long, - page: Int, - searchTerm: String?, - filters: List?, - ) = sourceRepository.getSearchResults( - sourceId, - page, - searchTerm?.ifBlank { null }, - filters, - ) - - companion object { - private val log = logging() +@Inject +class GetSearchManga( + private val sourceRepository: SourceRepository, +) { + suspend fun await( + source: Source, + page: Int, + searchTerm: String?, + filters: List?, + onError: suspend (Throwable) -> Unit = { + }, + ) = asFlow(source.id, page, searchTerm, filters) + .catch { + onError(it) + log.warn(it) { "Failed to get search results from ${source.displayName} on page $page with query '$searchTerm'" } } + .singleOrNull() + + suspend fun await( + sourceId: Long, + searchTerm: String?, + page: Int, + filters: List?, + onError: suspend (Throwable) -> Unit = { + }, + ) = asFlow(sourceId, page, searchTerm, filters) + .catch { + onError(it) + log.warn(it) { "Failed to get search results from $sourceId on page $page with query '$searchTerm'" } + } + .singleOrNull() + + fun asFlow( + source: Source, + page: Int, + searchTerm: String?, + filters: List?, + ) = sourceRepository.getSearchResults( + source.id, + page, + searchTerm?.ifBlank { null }, + filters, + ) + + fun asFlow( + sourceId: Long, + page: Int, + searchTerm: String?, + filters: List?, + ) = sourceRepository.getSearchResults( + sourceId, + page, + searchTerm?.ifBlank { null }, + filters, + ) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceList.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceList.kt index 4954093c..3e079bf5 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceList.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceList.kt @@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetSourceList - @Inject - constructor( - private val sourceRepository: SourceRepository, - ) { - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = - asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to get source list" } - } - .singleOrNull() +@Inject +class GetSourceList( + private val sourceRepository: SourceRepository, +) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to get source list" } + } + .singleOrNull() - fun asFlow() = sourceRepository.getSourceList() + fun asFlow() = sourceRepository.getSourceList() - companion object { - private val log = logging() - } + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceSettings.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceSettings.kt index c77c2d81..edecdb3b 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceSettings.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceSettings.kt @@ -13,36 +13,35 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetSourceSettings - @Inject - constructor( - private val sourceRepository: SourceRepository, - ) { - suspend fun await( - source: Source, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(source.id) - .catch { - onError(it) - log.warn(it) { "Failed to get source settings for ${source.displayName}" } - } - .singleOrNull() - - suspend fun await( - sourceId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(sourceId) - .catch { - onError(it) - log.warn(it) { "Failed to get source settings for $sourceId" } - } - .singleOrNull() - - fun asFlow(source: Source) = sourceRepository.getSourceSettings(source.id) - - fun asFlow(sourceId: Long) = sourceRepository.getSourceSettings(sourceId) - - companion object { - private val log = logging() +@Inject +class GetSourceSettings( + private val sourceRepository: SourceRepository, +) { + suspend fun await( + source: Source, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(source.id) + .catch { + onError(it) + log.warn(it) { "Failed to get source settings for ${source.displayName}" } } + .singleOrNull() + + suspend fun await( + sourceId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(sourceId) + .catch { + onError(it) + log.warn(it) { "Failed to get source settings for $sourceId" } + } + .singleOrNull() + + fun asFlow(source: Source) = sourceRepository.getSourceSettings(source.id) + + fun asFlow(sourceId: Long) = sourceRepository.getSourceSettings(sourceId) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SetSourceSetting.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SetSourceSetting.kt index 74fe3761..ca3f0d67 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SetSourceSetting.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SetSourceSetting.kt @@ -13,32 +13,31 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class SetSourceSetting - @Inject - constructor( - private val sourceRepository: SourceRepository, - ) { - suspend fun await( - sourceId: Long, - sourcePreference: SourcePreference, - onError: suspend (Throwable) -> Unit = { - }, - ) = asFlow(sourceId, sourcePreference) - .catch { - onError(it) - log.warn(it) { "Failed to set setting for $sourceId with index = ${sourcePreference.position}" } - } - .collect() - - fun asFlow( - sourceId: Long, - sourcePreference: SourcePreference, - ) = sourceRepository.setSourceSetting( - sourceId, - sourcePreference, - ) - - companion object { - private val log = logging() +@Inject +class SetSourceSetting( + private val sourceRepository: SourceRepository, +) { + suspend fun await( + sourceId: Long, + sourcePreference: SourcePreference, + onError: suspend (Throwable) -> Unit = { + }, + ) = asFlow(sourceId, sourcePreference) + .catch { + onError(it) + log.warn(it) { "Failed to set setting for $sourceId with index = ${sourcePreference.position}" } } + .collect() + + fun asFlow( + sourceId: Long, + sourcePreference: SourcePreference, + ) = sourceRepository.setSourceSetting( + sourceId, + sourcePreference, + ) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SourcePager.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SourcePager.kt index 899d7102..d0de2aa8 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SourcePager.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SourcePager.kt @@ -33,21 +33,21 @@ fun interface GetMangaPage { suspend fun get(page: Int): MangaPage? } -class SourcePager - @Inject - constructor( - private val getManga: GetManga, - private val serverListeners: ServerListeners, - @Assisted private val fetcher: GetMangaPage, - ) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) { - private val sourceMutex = Mutex() +@Inject +class SourcePager( + private val getManga: GetManga, + private val serverListeners: ServerListeners, + @Assisted private val fetcher: GetMangaPage, +) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) { + private val sourceMutex = Mutex() - private val _sourceManga = MutableStateFlow>(emptyList()) + private val _sourceManga = MutableStateFlow>(emptyList()) - private val mangaIds = _sourceManga.map { mangas -> mangas.map { it.id } } - .stateIn(this, SharingStarted.Eagerly, emptyList()) + private val mangaIds = _sourceManga.map { mangas -> mangas.map { it.id } } + .stateIn(this, SharingStarted.Eagerly, emptyList()) - private val changedManga = serverListeners.mangaListener.runningFold(emptyMap()) { manga, updatedMangaIds -> + private val changedManga = + serverListeners.mangaListener.runningFold(emptyMap()) { manga, updatedMangaIds -> coroutineScope { manga + updatedMangaIds.filter { it in mangaIds.value }.map { async { @@ -57,37 +57,37 @@ class SourcePager } }.stateIn(this, SharingStarted.Eagerly, emptyMap()) - val mangas = combine(_sourceManga, changedManga) { sourceManga, changedManga -> - sourceManga.map { changedManga[it.id] ?: it } - }.stateIn(this, SharingStarted.Eagerly, emptyList()) + val mangas = combine(_sourceManga, changedManga) { sourceManga, changedManga -> + sourceManga.map { changedManga[it.id] ?: it } + }.stateIn(this, SharingStarted.Eagerly, emptyList()) - private val _pageNum = MutableStateFlow(0) - val pageNum = _pageNum.asStateFlow() + private val _pageNum = MutableStateFlow(0) + val pageNum = _pageNum.asStateFlow() - private val _hasNextPage = MutableStateFlow(true) - val hasNextPage = _hasNextPage.asStateFlow() + private val _hasNextPage = MutableStateFlow(true) + val hasNextPage = _hasNextPage.asStateFlow() - private val _loading = MutableStateFlow(true) - val loading = _loading.asStateFlow() + private val _loading = MutableStateFlow(true) + val loading = _loading.asStateFlow() - fun loadNextPage() { - launch { - if (hasNextPage.value && sourceMutex.tryLock()) { - _pageNum.value++ - val page = fetcher.get(_pageNum.value) - if (page != null) { - _sourceManga.value = _sourceManga.value + page.mangaList - _hasNextPage.value = page.hasNextPage - } else { - _pageNum.value-- - } - sourceMutex.unlock() + fun loadNextPage() { + launch { + if (hasNextPage.value && sourceMutex.tryLock()) { + _pageNum.value++ + val page = fetcher.get(_pageNum.value) + if (page != null) { + _sourceManga.value = _sourceManga.value + page.mangaList + _hasNextPage.value = page.hasNextPage + } else { + _pageNum.value-- } - _loading.value = false + sourceMutex.unlock() } - } - - companion object { - private val log = logging() + _loading.value = false } } + + companion object { + private val log = logging() + } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ui/model/StartScreen.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ui/model/StartScreen.kt index 6507f54b..ef678b60 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ui/model/StartScreen.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ui/model/StartScreen.kt @@ -15,7 +15,7 @@ enum class StartScreen { Library, Updates, -// History, + // History, Sources, Extensions, } diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/GetRecentUpdates.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/GetRecentUpdates.kt index 01e65112..683487c1 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/GetRecentUpdates.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/GetRecentUpdates.kt @@ -12,24 +12,23 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetRecentUpdates - @Inject - constructor( - private val updatesRepository: UpdatesRepository, - ) { - suspend fun await( - pageNum: Int, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(pageNum) - .catch { - onError(it) - log.warn(it) { "Failed to get updates for page $pageNum" } - } - .singleOrNull() - - fun asFlow(pageNum: Int) = updatesRepository.getRecentUpdates(pageNum) - - companion object { - private val log = logging() +@Inject +class GetRecentUpdates( + private val updatesRepository: UpdatesRepository, +) { + suspend fun await( + pageNum: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(pageNum) + .catch { + onError(it) + log.warn(it) { "Failed to get updates for page $pageNum" } } + .singleOrNull() + + fun asFlow(pageNum: Int) = updatesRepository.getRecentUpdates(pageNum) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateCategory.kt index f4d26d60..27de0bef 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateCategory.kt @@ -13,36 +13,35 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateCategory - @Inject - constructor( - private val updatesRepository: UpdatesRepository, - ) { - suspend fun await( - categoryId: Long, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(categoryId) - .catch { - onError(it) - log.warn(it) { "Failed to update category $categoryId" } - } - .collect() - - suspend fun await( - category: Category, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(category) - .catch { - onError(it) - log.warn(it) { "Failed to update category ${category.name}(${category.id})" } - } - .collect() - - fun asFlow(categoryId: Long) = updatesRepository.updateCategory(categoryId) - - fun asFlow(category: Category) = updatesRepository.updateCategory(category.id) - - companion object { - private val log = logging() +@Inject +class UpdateCategory( + private val updatesRepository: UpdatesRepository, +) { + suspend fun await( + categoryId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(categoryId) + .catch { + onError(it) + log.warn(it) { "Failed to update category $categoryId" } } + .collect() + + suspend fun await( + category: Category, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(category) + .catch { + onError(it) + log.warn(it) { "Failed to update category ${category.name}(${category.id})" } + } + .collect() + + fun asFlow(categoryId: Long) = updatesRepository.updateCategory(categoryId) + + fun asFlow(category: Category) = updatesRepository.updateCategory(category.id) + + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateChecker.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateChecker.kt index b5edcef0..deeaa2c0 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateChecker.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateChecker.kt @@ -21,77 +21,76 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateChecker - @Inject - constructor( - private val updatePreferences: UpdatePreferences, - private val client: Http, - ) { - suspend fun await( - manualFetch: Boolean, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manualFetch) - .catch { - onError(it) - log.warn(it) { "Failed to check for updates" } - } - .singleOrNull() - - fun asFlow(manualFetch: Boolean) = - flow { - if (!manualFetch && !updatePreferences.enabled().get()) return@flow - val latestRelease = client.get( - "https://api.github.com/repos/$GITHUB_REPO/releases/latest", - ).body() - - if (isNewVersion(latestRelease.version)) { - emit(Update.UpdateFound(latestRelease)) - } else { - emit(Update.NoUpdatesFound) - } - }.flowOn(Dispatchers.IO) - - sealed class Update { - data class UpdateFound( - val release: GithubRelease, - ) : Update() - - data object NoUpdatesFound : Update() +@Inject +class UpdateChecker( + private val updatePreferences: UpdatePreferences, + private val client: Http, +) { + suspend fun await( + manualFetch: Boolean, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manualFetch) + .catch { + onError(it) + log.warn(it) { "Failed to check for updates" } } + .singleOrNull() - // Thanks to Tachiyomi for inspiration - private fun isNewVersion(versionTag: String): Boolean { - // Removes prefixes like "r" or "v" - val newVersion = versionTag.replace("[^\\d.]".toRegex(), "") + fun asFlow(manualFetch: Boolean) = + flow { + if (!manualFetch && !updatePreferences.enabled().get()) return@flow + val latestRelease = client.get( + "https://api.github.com/repos/$GITHUB_REPO/releases/latest", + ).body() - return if (BuildKonfig.IS_PREVIEW) { - // Preview builds: based on releases in "Suwayomi/Suwayomi-JUI-preview" repo - // tagged as something like "r123" - newVersion.toInt() > BuildKonfig.PREVIEW_BUILD + if (isNewVersion(latestRelease.version)) { + emit(Update.UpdateFound(latestRelease)) } else { - // Release builds: based on releases in "Suwayomi/Suwayomi-JUI" repo - // tagged as something like "v1.1.2" - newVersion != BuildKonfig.VERSION + emit(Update.NoUpdatesFound) } - } + }.flowOn(Dispatchers.IO) - companion object { - private val GITHUB_REPO = if (BuildKonfig.IS_PREVIEW) { - "Suwayomi/Suwayomi-JUI-preview" - } else { - "Suwayomi/Suwayomi-JUI" - } + sealed class Update { + data class UpdateFound( + val release: GithubRelease, + ) : Update() - private val RELEASE_TAG: String by lazy { - if (BuildKonfig.IS_PREVIEW) { - "r${BuildKonfig.PREVIEW_BUILD}" - } else { - "v${BuildKonfig.VERSION}" - } - } + data object NoUpdatesFound : Update() + } - val RELEASE_URL = "https://github.com/$GITHUB_REPO/releases/tag/$RELEASE_TAG" + // Thanks to Tachiyomi for inspiration + private fun isNewVersion(versionTag: String): Boolean { + // Removes prefixes like "r" or "v" + val newVersion = versionTag.replace("[^\\d.]".toRegex(), "") - private val log = logging() + return if (BuildKonfig.IS_PREVIEW) { + // Preview builds: based on releases in "Suwayomi/Suwayomi-JUI-preview" repo + // tagged as something like "r123" + newVersion.toInt() > BuildKonfig.PREVIEW_BUILD + } else { + // Release builds: based on releases in "Suwayomi/Suwayomi-JUI" repo + // tagged as something like "v1.1.2" + newVersion != BuildKonfig.VERSION } } + + companion object { + private val GITHUB_REPO = if (BuildKonfig.IS_PREVIEW) { + "Suwayomi/Suwayomi-JUI-preview" + } else { + "Suwayomi/Suwayomi-JUI" + } + + private val RELEASE_TAG: String by lazy { + if (BuildKonfig.IS_PREVIEW) { + "r${BuildKonfig.PREVIEW_BUILD}" + } else { + "v${BuildKonfig.VERSION}" + } + } + + val RELEASE_URL = "https://github.com/$GITHUB_REPO/releases/tag/$RELEASE_TAG" + + private val log = logging() + } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateLibrary.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateLibrary.kt index 4e1e0d33..b6881d02 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateLibrary.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateLibrary.kt @@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateLibrary - @Inject - constructor( - private val updatesRepository: UpdatesRepository, - ) { - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = - asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to update library" } - } - .collect() +@Inject +class UpdateLibrary( + private val updatesRepository: UpdatesRepository, +) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to update library" } + } + .collect() - fun asFlow() = updatesRepository.updateLibrary() + fun asFlow() = updatesRepository.updateLibrary() - companion object { - private val log = logging() - } + companion object { + private val log = logging() } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdatesPager.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdatesPager.kt index 16d87f43..068aa6bb 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdatesPager.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdatesPager.kt @@ -37,54 +37,54 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import me.tatarka.inject.annotations.Inject -class UpdatesPager - @Inject - constructor( - private val getRecentUpdates: GetRecentUpdates, - private val getManga: GetManga, - private val getChapter: GetChapter, - private val serverListeners: ServerListeners, - ) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) { - private val updatesMutex = Mutex() +@Inject +class UpdatesPager( + private val getRecentUpdates: GetRecentUpdates, + private val getManga: GetManga, + private val getChapter: GetChapter, + private val serverListeners: ServerListeners, +) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) { + private val updatesMutex = Mutex() - private val fetchedUpdates = MutableSharedFlow>() - private val foldedUpdates = fetchedUpdates.runningFold(emptyList()) { updates, newUpdates -> - updates.ifEmpty { - val first = newUpdates.firstOrNull()?.chapter ?: return@runningFold updates - listOf( - Updates.Date( - Instant.fromEpochSeconds(first.fetchedAt) - .toLocalDateTime(TimeZone.currentSystemDefault()) - .date, - ), - ) - } + newUpdates.fold(emptyList()) { list, (manga, chapter) -> - val date = (list.lastOrNull() as? Updates.Update)?.let { - val lastUpdateDate = Instant.fromEpochSeconds(it.chapter.fetchedAt) + private val fetchedUpdates = MutableSharedFlow>() + private val foldedUpdates = fetchedUpdates.runningFold(emptyList()) { updates, newUpdates -> + updates.ifEmpty { + val first = newUpdates.firstOrNull()?.chapter ?: return@runningFold updates + listOf( + Updates.Date( + Instant.fromEpochSeconds(first.fetchedAt) .toLocalDateTime(TimeZone.currentSystemDefault()) - .date - val chapterDate = Instant.fromEpochSeconds(chapter.fetchedAt) - .toLocalDateTime(TimeZone.currentSystemDefault()) - .date - chapterDate.takeUnless { it == lastUpdateDate } - } - - if (date == null) { - list + Updates.Update(manga, chapter) - } else { - list + Updates.Date(date) + Updates.Update(manga, chapter) - } + .date, + ), + ) + } + newUpdates.fold(emptyList()) { list, (manga, chapter) -> + val date = (list.lastOrNull() as? Updates.Update)?.let { + val lastUpdateDate = Instant.fromEpochSeconds(it.chapter.fetchedAt) + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date + val chapterDate = Instant.fromEpochSeconds(chapter.fetchedAt) + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date + chapterDate.takeUnless { it == lastUpdateDate } } - }.stateIn(this, SharingStarted.Eagerly, emptyList()) - private val mangaIds = foldedUpdates.map { updates -> - updates.filterIsInstance().map { it.manga.id } - }.stateIn(this, SharingStarted.Eagerly, emptyList()) - private val chapterIds = foldedUpdates.map { updates -> - updates.filterIsInstance().map { it.chapter.id } - }.stateIn(this, SharingStarted.Eagerly, emptyList()) + if (date == null) { + list + Updates.Update(manga, chapter) + } else { + list + Updates.Date(date) + Updates.Update(manga, chapter) + } + } + }.stateIn(this, SharingStarted.Eagerly, emptyList()) - private val changedManga = serverListeners.mangaListener.runningFold(emptyMap()) { manga, updatedMangaIds -> + private val mangaIds = foldedUpdates.map { updates -> + updates.filterIsInstance().map { it.manga.id } + }.stateIn(this, SharingStarted.Eagerly, emptyList()) + private val chapterIds = foldedUpdates.map { updates -> + updates.filterIsInstance().map { it.chapter.id } + }.stateIn(this, SharingStarted.Eagerly, emptyList()) + + private val changedManga = + serverListeners.mangaListener.runningFold(emptyMap()) { manga, updatedMangaIds -> coroutineScope { manga + updatedMangaIds.filter { it in mangaIds.value }.map { async { @@ -94,82 +94,82 @@ class UpdatesPager } }.stateIn(this, SharingStarted.Eagerly, emptyMap()) - private val changedChapters = MutableStateFlow(emptyMap()) + private val changedChapters = MutableStateFlow(emptyMap()) - init { - serverListeners.chapterIdsListener - .onEach { updatedChapterIds -> - val chapters = coroutineScope { - updatedChapterIds.mapNotNull { id -> chapterIds.value.find { it == id } }.map { - async { - getChapter.await(it) - } - }.awaitAll().filterNotNull().associateBy { it.id } - } - changedChapters.update { it + chapters } - } - .launchIn(this) - } - - val updates = combine( - foldedUpdates, - changedManga, - changedChapters, - ) { updates, changedManga, changedChapters -> - updates.map { - when (it) { - is Updates.Date -> it - - is Updates.Update -> it.copy( - manga = changedManga[it.manga.id] ?: it.manga, - chapter = changedChapters[it.chapter.id] ?: it.chapter, - ) + init { + serverListeners.chapterIdsListener + .onEach { updatedChapterIds -> + val chapters = coroutineScope { + updatedChapterIds.mapNotNull { id -> chapterIds.value.find { it == id } }.map { + async { + getChapter.await(it) + } + }.awaitAll().filterNotNull().associateBy { it.id } } + changedChapters.update { it + chapters } } - }.stateIn(this, SharingStarted.Eagerly, emptyList()) + .launchIn(this) + } - private val currentPage = MutableStateFlow(0) - private val hasNextPage = MutableStateFlow(true) + val updates = combine( + foldedUpdates, + changedManga, + changedChapters, + ) { updates, changedManga, changedChapters -> + updates.map { + when (it) { + is Updates.Date -> it + + is Updates.Update -> it.copy( + manga = changedManga[it.manga.id] ?: it.manga, + chapter = changedChapters[it.chapter.id] ?: it.chapter, + ) + } + } + }.stateIn(this, SharingStarted.Eagerly, emptyList()) + + private val currentPage = MutableStateFlow(0) + private val hasNextPage = MutableStateFlow(true) + + @Immutable + sealed class Updates { + @Immutable + data class Update( + val manga: Manga, + val chapter: Chapter, + ) : Updates() @Immutable - sealed class Updates { - @Immutable - data class Update( - val manga: Manga, - val chapter: Chapter, - ) : Updates() - - @Immutable - data class Date( - val date: String, - ) : Updates() { - constructor(date: LocalDate) : this(date.toString()) - } - } - - fun loadNextPage( - onComplete: (() -> Unit)? = null, - onError: suspend (Throwable) -> Unit, - ) { - launch { - if (hasNextPage.value && updatesMutex.tryLock()) { - currentPage.value++ - if (!getUpdates(currentPage.value, onError)) { - currentPage.value-- - } - updatesMutex.unlock() - } - onComplete?.invoke() - } - } - - private suspend fun getUpdates( - page: Int, - onError: suspend (Throwable) -> Unit, - ): Boolean { - val updates = getRecentUpdates.await(page, onError) ?: return false - hasNextPage.value = updates.hasNextPage - fetchedUpdates.emit(updates.page) - return true + data class Date( + val date: String, + ) : Updates() { + constructor(date: LocalDate) : this(date.toString()) } } + + fun loadNextPage( + onComplete: (() -> Unit)? = null, + onError: suspend (Throwable) -> Unit, + ) { + launch { + if (hasNextPage.value && updatesMutex.tryLock()) { + currentPage.value++ + if (!getUpdates(currentPage.value, onError)) { + currentPage.value-- + } + updatesMutex.unlock() + } + onComplete?.invoke() + } + } + + private suspend fun getUpdates( + page: Int, + onError: suspend (Throwable) -> Unit, + ): Boolean { + val updates = getRecentUpdates.await(page, onError) ?: return false + hasNextPage.value = updates.hasNextPage + fetchedUpdates.emit(updates.page) + return true + } +} diff --git a/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerService.kt b/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerService.kt index f3a8b966..5d507664 100644 --- a/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerService.kt +++ b/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerService.kt @@ -38,169 +38,168 @@ import kotlin.io.path.exists import kotlin.io.path.isExecutable @OptIn(DelicateCoroutinesApi::class) -class ServerService - @Inject - constructor( - private val serverHostPreferences: ServerHostPreferences, - ) { - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) +@Inject +class ServerService( + private val serverHostPreferences: ServerHostPreferences, +) { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - private val host = serverHostPreferences.host().stateIn(GlobalScope) - private val _initialized = MutableStateFlow( - if (host.value) { - ServerResult.STARTING - } else { - ServerResult.UNUSED - }, - ) - val initialized = _initialized.asStateFlow() - private var process: Process? = null + private val host = serverHostPreferences.host().stateIn(GlobalScope) + private val _initialized = MutableStateFlow( + if (host.value) { + ServerResult.STARTING + } else { + ServerResult.UNUSED + }, + ) + val initialized = _initialized.asStateFlow() + private var process: Process? = null - fun startAnyway() { - _initialized.value = ServerResult.UNUSED - } + fun startAnyway() { + _initialized.value = ServerResult.UNUSED + } - @Throws(IOException::class) - private suspend fun copyJar(jarFile: Path) { - javaClass.getResourceAsStream("/Tachidesk.jar")?.source() - ?.copyTo(FileSystem.SYSTEM.sink(jarFile).buffer()) - } + @Throws(IOException::class) + private suspend fun copyJar(jarFile: Path) { + javaClass.getResourceAsStream("/Tachidesk.jar")?.source() + ?.copyTo(FileSystem.SYSTEM.sink(jarFile).buffer()) + } - private fun getJavaFromPath(javaPath: Path): String? { - val javaExeFile = javaPath.resolve("java.exe").toNioPath() - val javaUnixFile = javaPath.resolve("java").toNioPath() - return when { - javaExeFile.exists() && javaExeFile.isExecutable() -> javaExeFile.absolutePathString() - javaUnixFile.exists() && javaUnixFile.isExecutable() -> javaUnixFile.absolutePathString() - else -> null - } - } - - private fun getRuntimeJava(): String? = System.getProperty("java.home")?.let { getJavaFromPath(it.toPath().resolve("bin")) } - - private fun getPossibleJava(): String? = - System.getProperty("java.library.path")?.split(pathSeparatorChar) - .orEmpty() - .asSequence() - .mapNotNull { - val file = it.toPath() - if (file.toString().contains("java") || file.toString().contains("jdk")) { - if (file.name.equals("bin", true)) { - file - } else { - file.resolve("bin") - } - } else { - null - } - } - .mapNotNull { getJavaFromPath(it) } - .firstOrNull() - - private suspend fun runService() { - process?.destroy() - withIOContext { - process?.waitFor() - } - _initialized.value = if (host.value) { - ServerResult.STARTING - } else { - ServerResult.UNUSED - return - } - - val jarFile = userDataDir / "Tachidesk.jar" - if (!FileSystem.SYSTEM.exists(jarFile)) { - log.info { "Copying server to resources" } - withIOContext { copyJar(jarFile) } - } else { - try { - val jarVersion = withIOContext { - JarInputStream(FileSystem.SYSTEM.source(jarFile).buffer().inputStream()).use { jar -> - jar.manifest?.mainAttributes?.getValue("JUI-KEY")?.toIntOrNull() - } - } - - if (jarVersion != BuildKonfig.SERVER_CODE) { - log.info { "Updating server file from resources" } - withIOContext { copyJar(jarFile) } - } - } catch (e: IOException) { - log.error(e) { - "Error accessing server jar, cannot update server, ${BuildKonfig.NAME} may not work properly" - } - } - } - - val javaPath = getRuntimeJava() ?: getPossibleJava() ?: "java" - log.info { "Starting server with $javaPath" } - val properties = serverHostPreferences.properties() - log.info { "Using server properties:\n" + properties.joinToString(separator = "\n") } - - withIOContext { - val reader: Reader - process = ProcessBuilder(javaPath, *properties, "-jar", jarFile.toString()) - .redirectErrorStream(true) - .start() - .also { - reader = it.inputStream.reader() - } - log.info { "Server started successfully" } - val log = logging("Server") - reader.forEachLine { - if (_initialized.value == ServerResult.STARTING) { - when { - it.contains("Javalin started") -> - _initialized.value = ServerResult.STARTED - - it.contains("Javalin has stopped") -> - _initialized.value = ServerResult.FAILED - } - } - log.info { it } - } - if (_initialized.value == ServerResult.STARTING) { - _initialized.value = ServerResult.FAILED - } - log.info { "Server closed" } - val exitVal = process?.waitFor() - log.info { "Process exitValue: $exitVal" } - process = null - } - } - - fun startServer() { - scope.coroutineContext.cancelChildren() - host - .mapLatest { - runService() - } - .catch { - log.error(it) { "Error launching Tachidesk.jar" } - if (_initialized.value == ServerResult.STARTING || _initialized.value == ServerResult.STARTED) { - _initialized.value = ServerResult.FAILED - } - } - .launchIn(scope) - } - - init { - Runtime.getRuntime().addShutdownHook( - thread(start = false) { - process?.destroy() - process = null - }, - ) - } - - enum class ServerResult { - UNUSED, - STARTING, - STARTED, - FAILED, - } - - private companion object { - private val log = logging() + private fun getJavaFromPath(javaPath: Path): String? { + val javaExeFile = javaPath.resolve("java.exe").toNioPath() + val javaUnixFile = javaPath.resolve("java").toNioPath() + return when { + javaExeFile.exists() && javaExeFile.isExecutable() -> javaExeFile.absolutePathString() + javaUnixFile.exists() && javaUnixFile.isExecutable() -> javaUnixFile.absolutePathString() + else -> null } } + + private fun getRuntimeJava(): String? = System.getProperty("java.home")?.let { getJavaFromPath(it.toPath().resolve("bin")) } + + private fun getPossibleJava(): String? = + System.getProperty("java.library.path")?.split(pathSeparatorChar) + .orEmpty() + .asSequence() + .mapNotNull { + val file = it.toPath() + if (file.toString().contains("java") || file.toString().contains("jdk")) { + if (file.name.equals("bin", true)) { + file + } else { + file.resolve("bin") + } + } else { + null + } + } + .mapNotNull { getJavaFromPath(it) } + .firstOrNull() + + private suspend fun runService() { + process?.destroy() + withIOContext { + process?.waitFor() + } + _initialized.value = if (host.value) { + ServerResult.STARTING + } else { + ServerResult.UNUSED + return + } + + val jarFile = userDataDir / "Tachidesk.jar" + if (!FileSystem.SYSTEM.exists(jarFile)) { + log.info { "Copying server to resources" } + withIOContext { copyJar(jarFile) } + } else { + try { + val jarVersion = withIOContext { + JarInputStream(FileSystem.SYSTEM.source(jarFile).buffer().inputStream()).use { jar -> + jar.manifest?.mainAttributes?.getValue("JUI-KEY")?.toIntOrNull() + } + } + + if (jarVersion != BuildKonfig.SERVER_CODE) { + log.info { "Updating server file from resources" } + withIOContext { copyJar(jarFile) } + } + } catch (e: IOException) { + log.error(e) { + "Error accessing server jar, cannot update server, ${BuildKonfig.NAME} may not work properly" + } + } + } + + val javaPath = getRuntimeJava() ?: getPossibleJava() ?: "java" + log.info { "Starting server with $javaPath" } + val properties = serverHostPreferences.properties() + log.info { "Using server properties:\n" + properties.joinToString(separator = "\n") } + + withIOContext { + val reader: Reader + process = ProcessBuilder(javaPath, *properties, "-jar", jarFile.toString()) + .redirectErrorStream(true) + .start() + .also { + reader = it.inputStream.reader() + } + log.info { "Server started successfully" } + val log = logging("Server") + reader.forEachLine { + if (_initialized.value == ServerResult.STARTING) { + when { + it.contains("Javalin started") -> + _initialized.value = ServerResult.STARTED + + it.contains("Javalin has stopped") -> + _initialized.value = ServerResult.FAILED + } + } + log.info { it } + } + if (_initialized.value == ServerResult.STARTING) { + _initialized.value = ServerResult.FAILED + } + log.info { "Server closed" } + val exitVal = process?.waitFor() + log.info { "Process exitValue: $exitVal" } + process = null + } + } + + fun startServer() { + scope.coroutineContext.cancelChildren() + host + .mapLatest { + runService() + } + .catch { + log.error(it) { "Error launching Tachidesk.jar" } + if (_initialized.value == ServerResult.STARTING || _initialized.value == ServerResult.STARTED) { + _initialized.value = ServerResult.FAILED + } + } + .launchIn(scope) + } + + init { + Runtime.getRuntime().addShutdownHook( + thread(start = false) { + process?.destroy() + process = null + }, + ) + } + + enum class ServerResult { + UNUSED, + STARTING, + STARTED, + FAILED, + } + + private companion object { + private val log = logging() + } +} diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/settings/AndroidSettingsServerScreen.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/settings/AndroidSettingsServerScreen.kt index 3f98c566..e3e49b24 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/settings/AndroidSettingsServerScreen.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/settings/AndroidSettingsServerScreen.kt @@ -15,8 +15,7 @@ import me.tatarka.inject.annotations.Inject @Composable actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostViewModel): LazyListScope.() -> Unit = {} -actual class SettingsServerHostViewModel - @Inject - constructor( - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) +@Inject +actual class SettingsServerHostViewModel( + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt index 43c961a0..39293cfc 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt @@ -30,104 +30,103 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import me.tatarka.inject.annotations.Inject -class ImageLoaderProvider - @Inject - constructor( - private val http: Http, - serverPreferences: ServerPreferences, - private val context: ContextWrapper, - ) { - @OptIn(DelicateCoroutinesApi::class) - val serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope) +@Inject +class ImageLoaderProvider( + private val http: Http, + serverPreferences: ServerPreferences, + private val context: ContextWrapper, +) { + @OptIn(DelicateCoroutinesApi::class) + val serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope) - fun get(imageCache: ImageCache): ImageLoader = - ImageLoader { - components { - register(context, http) - add(MokoResourceFetcher.Factory()) - add(MangaCoverMapper()) - add(MangaCoverKeyer()) - add(ExtensionIconMapper()) - add(ExtensionIconKeyer()) - add(SourceIconMapper()) - add(SourceIconKeyer()) - } - options { - configure(context) - } - interceptor { - diskCache { imageCache } - bitmapMemoryCacheConfig { configure(context) } - } + fun get(imageCache: ImageCache): ImageLoader = + ImageLoader { + components { + register(context, http) + add(MokoResourceFetcher.Factory()) + add(MangaCoverMapper()) + add(MangaCoverKeyer()) + add(ExtensionIconMapper()) + add(ExtensionIconKeyer()) + add(SourceIconMapper()) + add(SourceIconKeyer()) } - - inner class MangaCoverMapper : Mapper { - override fun map( - data: Any, - options: Options, - ): Url? { - if (data !is Manga) return null - if (data.thumbnailUrl.isNullOrBlank()) return null - return Url(serverUrl.value.toString() + data.thumbnailUrl) + options { + configure(context) + } + interceptor { + diskCache { imageCache } + bitmapMemoryCacheConfig { configure(context) } } } - class MangaCoverKeyer : Keyer { - override fun key( - data: Any, - options: Options, - type: Keyer.Type, - ): String? { - if (data !is Manga) return null - return "${data.sourceId}-${data.thumbnailUrl}-${data.thumbnailUrlLastFetched}" - } - } - - inner class ExtensionIconMapper : Mapper { - override fun map( - data: Any, - options: Options, - ): Url? { - if (data !is Extension) return null - if (data.iconUrl.isBlank()) return null - return Url("${serverUrl.value}${data.iconUrl}") - } - } - - class ExtensionIconKeyer : Keyer { - override fun key( - data: Any, - options: Options, - type: Keyer.Type, - ): String? { - if (data !is Extension) return null - return data.iconUrl - } - } - - inner class SourceIconMapper : Mapper { - override fun map( - data: Any, - options: Options, - ): Url? { - if (data !is Source) return null - if (data.iconUrl.isBlank()) return null - return Url(serverUrl.value.toString() + data.iconUrl) - } - } - - class SourceIconKeyer : Keyer { - override fun key( - data: Any, - options: Options, - type: Keyer.Type, - ): String? { - if (data !is Source) return null - return data.iconUrl - } + inner class MangaCoverMapper : Mapper { + override fun map( + data: Any, + options: Options, + ): Url? { + if (data !is Manga) return null + if (data.thumbnailUrl.isNullOrBlank()) return null + return Url(serverUrl.value.toString() + data.thumbnailUrl) } } + class MangaCoverKeyer : Keyer { + override fun key( + data: Any, + options: Options, + type: Keyer.Type, + ): String? { + if (data !is Manga) return null + return "${data.sourceId}-${data.thumbnailUrl}-${data.thumbnailUrlLastFetched}" + } + } + + inner class ExtensionIconMapper : Mapper { + override fun map( + data: Any, + options: Options, + ): Url? { + if (data !is Extension) return null + if (data.iconUrl.isBlank()) return null + return Url("${serverUrl.value}${data.iconUrl}") + } + } + + class ExtensionIconKeyer : Keyer { + override fun key( + data: Any, + options: Options, + type: Keyer.Type, + ): String? { + if (data !is Extension) return null + return data.iconUrl + } + } + + inner class SourceIconMapper : Mapper { + override fun map( + data: Any, + options: Options, + ): Url? { + if (data !is Source) return null + if (data.iconUrl.isBlank()) return null + return Url(serverUrl.value.toString() + data.iconUrl) + } + } + + class SourceIconKeyer : Keyer { + override fun key( + data: Any, + options: Options, + type: Keyer.Type, + ): String? { + if (data !is Source) return null + return data.iconUrl + } + } +} + expect fun OptionsBuilder.configure(contextWrapper: ContextWrapper) expect fun ComponentRegistryBuilder.register( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/theme/AppTheme.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/theme/AppTheme.kt index 426babed..9cb17407 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/theme/AppTheme.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/theme/AppTheme.kt @@ -59,100 +59,102 @@ fun AppTheme(content: @Composable () -> Unit) { } } -class AppThemeViewModel - @Inject - constructor( - private val uiPreferences: UiPreferences, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - override val scope = MainScope() +@Inject +class AppThemeViewModel( + private val uiPreferences: UiPreferences, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + override val scope = MainScope() - private val themeMode = uiPreferences.themeMode().asStateFlow() - private val lightTheme = uiPreferences.lightTheme().asStateFlow() - private val darkTheme = uiPreferences.darkTheme().asStateFlow() + private val themeMode = uiPreferences.themeMode().asStateFlow() + private val lightTheme = uiPreferences.lightTheme().asStateFlow() + private val darkTheme = uiPreferences.darkTheme().asStateFlow() - private val baseThemeJob = SupervisorJob() - private val baseThemeScope = CoroutineScope(baseThemeJob) + private val baseThemeJob = SupervisorJob() + private val baseThemeScope = CoroutineScope(baseThemeJob) - @Composable - fun getColors(): Pair { - val themeMode by themeMode.collectAsState() - val lightTheme by lightTheme.collectAsState() - val darkTheme by darkTheme.collectAsState() + @Composable + fun getColors(): Pair { + val themeMode by themeMode.collectAsState() + val lightTheme by lightTheme.collectAsState() + val darkTheme by darkTheme.collectAsState() - val baseTheme = getBaseTheme(themeMode, lightTheme, darkTheme) - val colors = remember(baseTheme.colors.isLight) { - baseThemeJob.cancelChildren() + val baseTheme = getBaseTheme(themeMode, lightTheme, darkTheme) + val colors = remember(baseTheme.colors.isLight) { + baseThemeJob.cancelChildren() - if (baseTheme.colors.isLight) { - uiPreferences.getLightColors().asStateFlow(baseThemeScope) - } else { - uiPreferences.getDarkColors().asStateFlow(baseThemeScope) - } - } - - val primary by colors.primaryStateFlow.collectAsState() - val secondary by colors.secondaryStateFlow.collectAsState() - val tertiary by colors.tertiaryStateFlow.collectAsState() - - return getMaterialColors(baseTheme.colors, primary, secondary) to getExtraColors(baseTheme.extraColors, tertiary) - } - - @Composable - private fun getBaseTheme( - themeMode: ThemeMode, - lightTheme: Int, - darkTheme: Int, - ): Theme { - fun getTheme( - id: Int, - isLight: Boolean, - ): Theme = - themes.find { it.id == id && it.colors.isLight == isLight } - ?: themes.first { it.colors.isLight == isLight } - - return when (themeMode) { - ThemeMode.System -> if (!isSystemInDarkTheme()) { - getTheme(lightTheme, true) - } else { - getTheme(darkTheme, false) - } - - ThemeMode.Light -> getTheme(lightTheme, true) - - ThemeMode.Dark -> getTheme(darkTheme, false) + if (baseTheme.colors.isLight) { + uiPreferences.getLightColors().asStateFlow(baseThemeScope) + } else { + uiPreferences.getDarkColors().asStateFlow(baseThemeScope) } } - private fun getMaterialColors( - baseColors: Colors, - colorPrimary: Color, - colorSecondary: Color, - ): Colors { - val primary = colorPrimary.takeOrElse { baseColors.primary } - val secondary = colorSecondary.takeOrElse { baseColors.secondary } - return baseColors.copy( - primary = primary, - primaryVariant = primary, - secondary = secondary, - secondaryVariant = secondary, - onPrimary = if (primary.luminance() > 0.5) Color.Black else Color.White, - onSecondary = if (secondary.luminance() > 0.5) Color.Black else Color.White, - ) - } + val primary by colors.primaryStateFlow.collectAsState() + val secondary by colors.secondaryStateFlow.collectAsState() + val tertiary by colors.tertiaryStateFlow.collectAsState() - private fun getExtraColors( - baseExtraColors: ExtraColors, - colorTertiary: Color, - ): ExtraColors { - val tertiary = colorTertiary.takeOrElse { baseExtraColors.tertiary } - return baseExtraColors.copy( - tertiary = tertiary, - ) - } + return getMaterialColors(baseTheme.colors, primary, secondary) to getExtraColors( + baseTheme.extraColors, + tertiary, + ) + } - override fun onDispose() { - baseThemeScope.cancel() - scope.cancel() + @Composable + private fun getBaseTheme( + themeMode: ThemeMode, + lightTheme: Int, + darkTheme: Int, + ): Theme { + fun getTheme( + id: Int, + isLight: Boolean, + ): Theme = + themes.find { it.id == id && it.colors.isLight == isLight } + ?: themes.first { it.colors.isLight == isLight } + + return when (themeMode) { + ThemeMode.System -> if (!isSystemInDarkTheme()) { + getTheme(lightTheme, true) + } else { + getTheme(darkTheme, false) + } + + ThemeMode.Light -> getTheme(lightTheme, true) + + ThemeMode.Dark -> getTheme(darkTheme, false) } } + + private fun getMaterialColors( + baseColors: Colors, + colorPrimary: Color, + colorSecondary: Color, + ): Colors { + val primary = colorPrimary.takeOrElse { baseColors.primary } + val secondary = colorSecondary.takeOrElse { baseColors.secondary } + return baseColors.copy( + primary = primary, + primaryVariant = primary, + secondary = secondary, + secondaryVariant = secondary, + onPrimary = if (primary.luminance() > 0.5) Color.Black else Color.White, + onSecondary = if (secondary.luminance() > 0.5) Color.Black else Color.White, + ) + } + + private fun getExtraColors( + baseExtraColors: ExtraColors, + colorTertiary: Color, + ): ExtraColors { + val tertiary = colorTertiary.takeOrElse { baseExtraColors.tertiary } + return baseExtraColors.copy( + tertiary = tertiary, + ) + } + + override fun onDispose() { + baseThemeScope.cancel() + scope.cancel() + } +} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesScreenViewModel.kt index 4147c275..fec47af0 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesScreenViewModel.kt @@ -27,121 +27,126 @@ import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class CategoriesScreenViewModel - @Inject - constructor( - private val getCategories: GetCategories, - private val createCategory: CreateCategory, - private val deleteCategory: DeleteCategory, - private val modifyCategory: ModifyCategory, - private val reorderCategory: ReorderCategory, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - private var originalCategories = emptyList() - private val _categories = MutableStateFlow>(persistentListOf()) - val categories = _categories.asStateFlow() +@Inject +class CategoriesScreenViewModel( + private val getCategories: GetCategories, + private val createCategory: CreateCategory, + private val deleteCategory: DeleteCategory, + private val modifyCategory: ModifyCategory, + private val reorderCategory: ReorderCategory, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + private var originalCategories = emptyList() + private val _categories = MutableStateFlow>(persistentListOf()) + val categories = _categories.asStateFlow() - init { - scope.launch { - getCategories() - } - } - - private suspend fun getCategories() { - _categories.value = persistentListOf() - val categories = getCategories.await(true, onError = { toast(it.message.orEmpty()) }) - if (categories != null) { - _categories.value = categories - .sortedBy { it.order } - .also { originalCategories = it } - .map { it.toMenuCategory() } - .toImmutableList() - } - } - - suspend fun updateRemoteCategories(manualUpdate: Boolean = false) { - val categories = _categories.value - val newCategories = categories.filter { it.id == null } - newCategories.forEach { - createCategory.await(it.name, onError = { toast(it.message.orEmpty()) }) - } - originalCategories.forEach { originalCategory -> - val category = categories.find { it.id == originalCategory.id } - if (category == null) { - deleteCategory.await(originalCategory, onError = { toast(it.message.orEmpty()) }) - } else if (category.name != originalCategory.name) { - modifyCategory.await(originalCategory, category.name, onError = { toast(it.message.orEmpty()) }) - } - } - var updatedCategories = getCategories.await(true, onError = { toast(it.message.orEmpty()) }) - categories.sortedBy { it.order }.forEach { category -> - val updatedCategory = updatedCategories?.find { it.id == category.id || it.name == category.name } ?: return@forEach - if (category.order != updatedCategory.order) { - log.debug { "${category.name}: ${updatedCategory.order} to ${category.order}" } - reorderCategory.await(category.id!!, category.order, onError = { toast(it.message.orEmpty()) }) - } - updatedCategories = getCategories.await(true, onError = { toast(it.message.orEmpty()) }) - } - - if (manualUpdate) { - getCategories() - } - } - - fun renameCategory( - category: MenuCategory, - newName: String, - ) { - _categories.value = (_categories.value.toPersistentList() - category + category.copy(name = newName)).sortedBy { it.order } - .toImmutableList() - } - - fun deleteCategory(category: MenuCategory) { - _categories.value = _categories.value.toPersistentList() - category - } - - fun createCategory(name: String) { - _categories.value = - _categories.value.toPersistentList() + MenuCategory(order = categories.value.size + 1, name = name, default = false) - } - - fun moveUp(category: MenuCategory) { - val categories = _categories.value.toMutableList() - val index = categories.indexOf(category) - if (index == -1) throw Exception("Invalid index") - categories.add(index - 1, categories.removeAt(index)) - _categories.value = categories - .mapIndexed { i, menuCategory -> - menuCategory.copy(order = i + 1) - } - .sortedBy { it.order } - .toImmutableList() - } - - fun moveDown(category: MenuCategory) { - val categories = _categories.value.toMutableList() - val index = categories.indexOf(category) - if (index == -1) throw Exception("Invalid index") - categories.add(index + 1, categories.removeAt(index)) - _categories.value = categories - .mapIndexed { i, menuCategory -> - menuCategory.copy(order = i + 1) - } - .sortedBy { it.order } - .toImmutableList() - } - - private fun Category.toMenuCategory() = MenuCategory(id, order, name, default) - - @Stable - data class MenuCategory( - val id: Long? = null, - val order: Int, - val name: String, - val default: Boolean = false, - ) - - private companion object { - private val log = logging() + init { + scope.launch { + getCategories() } } + + private suspend fun getCategories() { + _categories.value = persistentListOf() + val categories = getCategories.await(true, onError = { toast(it.message.orEmpty()) }) + if (categories != null) { + _categories.value = categories + .sortedBy { it.order } + .also { originalCategories = it } + .map { it.toMenuCategory() } + .toImmutableList() + } + } + + suspend fun updateRemoteCategories(manualUpdate: Boolean = false) { + val categories = _categories.value + val newCategories = categories.filter { it.id == null } + newCategories.forEach { + createCategory.await(it.name, onError = { toast(it.message.orEmpty()) }) + } + originalCategories.forEach { originalCategory -> + val category = categories.find { it.id == originalCategory.id } + if (category == null) { + deleteCategory.await(originalCategory, onError = { toast(it.message.orEmpty()) }) + } else if (category.name != originalCategory.name) { + modifyCategory.await(originalCategory, category.name, onError = { toast(it.message.orEmpty()) }) + } + } + var updatedCategories = getCategories.await(true, onError = { toast(it.message.orEmpty()) }) + categories.sortedBy { it.order }.forEach { category -> + val updatedCategory = + updatedCategories?.find { it.id == category.id || it.name == category.name } ?: return@forEach + if (category.order != updatedCategory.order) { + log.debug { "${category.name}: ${updatedCategory.order} to ${category.order}" } + reorderCategory.await(category.id!!, category.order, onError = { toast(it.message.orEmpty()) }) + } + updatedCategories = getCategories.await(true, onError = { toast(it.message.orEmpty()) }) + } + + if (manualUpdate) { + getCategories() + } + } + + fun renameCategory( + category: MenuCategory, + newName: String, + ) { + _categories.value = + (_categories.value.toPersistentList() - category + category.copy(name = newName)).sortedBy { it.order } + .toImmutableList() + } + + fun deleteCategory(category: MenuCategory) { + _categories.value = _categories.value.toPersistentList() - category + } + + fun createCategory(name: String) { + _categories.value = + _categories.value.toPersistentList() + MenuCategory( + order = categories.value.size + 1, + name = name, + default = false, + ) + } + + fun moveUp(category: MenuCategory) { + val categories = _categories.value.toMutableList() + val index = categories.indexOf(category) + if (index == -1) throw Exception("Invalid index") + categories.add(index - 1, categories.removeAt(index)) + _categories.value = categories + .mapIndexed { i, menuCategory -> + menuCategory.copy(order = i + 1) + } + .sortedBy { it.order } + .toImmutableList() + } + + fun moveDown(category: MenuCategory) { + val categories = _categories.value.toMutableList() + val index = categories.indexOf(category) + if (index == -1) throw Exception("Invalid index") + categories.add(index + 1, categories.removeAt(index)) + _categories.value = categories + .mapIndexed { i, menuCategory -> + menuCategory.copy(order = i + 1) + } + .sortedBy { it.order } + .toImmutableList() + } + + private fun Category.toMenuCategory() = MenuCategory(id, order, name, default) + + @Stable + data class MenuCategory( + val id: Long? = null, + val order: Int, + val name: String, + val default: Boolean = false, + ) + + private companion object { + private val log = logging() + } +} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/DownloadsScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/DownloadsScreenViewModel.kt index ac5541ff..4c945db1 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/DownloadsScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/DownloadsScreenViewModel.kt @@ -31,85 +31,90 @@ import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class DownloadsScreenViewModel - @Inject - constructor( - private val downloadService: DownloadService, - private val startDownloading: StartDownloading, - private val stopDownloading: StopDownloading, - private val clearDownloadQueue: ClearDownloadQueue, - private val queueChapterDownload: QueueChapterDownload, - private val stopChapterDownload: StopChapterDownload, - private val reorderChapterDownload: ReorderChapterDownload, - private val contextWrapper: ContextWrapper, - @Assisted standalone: Boolean, - ) : ViewModel(contextWrapper) { - private val uiScope = if (standalone) { - MainScope() - } else { - null - } +@Inject +class DownloadsScreenViewModel( + private val downloadService: DownloadService, + private val startDownloading: StartDownloading, + private val stopDownloading: StopDownloading, + private val clearDownloadQueue: ClearDownloadQueue, + private val queueChapterDownload: QueueChapterDownload, + private val stopChapterDownload: StopChapterDownload, + private val reorderChapterDownload: ReorderChapterDownload, + private val contextWrapper: ContextWrapper, + @Assisted standalone: Boolean, +) : ViewModel(contextWrapper) { + private val uiScope = if (standalone) { + MainScope() + } else { + null + } - override val scope: CoroutineScope - get() = uiScope ?: super.scope + override val scope: CoroutineScope + get() = uiScope ?: super.scope - val serviceStatus = DownloadService.status.asStateFlow() - val downloaderStatus = DownloadService.downloaderStatus.asStateFlow() - val downloadQueue = DownloadService.downloadQueue.map { it.toImmutableList() } - .stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + val serviceStatus = DownloadService.status.asStateFlow() + val downloaderStatus = DownloadService.downloaderStatus.asStateFlow() + val downloadQueue = DownloadService.downloadQueue.map { it.toImmutableList() } + .stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - fun start() { - scope.launch { startDownloading.await(onError = { toast(it.message.orEmpty()) }) } - } + fun start() { + scope.launch { startDownloading.await(onError = { toast(it.message.orEmpty()) }) } + } - fun pause() { - scope.launch { stopDownloading.await(onError = { toast(it.message.orEmpty()) }) } - } + fun pause() { + scope.launch { stopDownloading.await(onError = { toast(it.message.orEmpty()) }) } + } - fun clear() { - scope.launch { clearDownloadQueue.await(onError = { toast(it.message.orEmpty()) }) } - } + fun clear() { + scope.launch { clearDownloadQueue.await(onError = { toast(it.message.orEmpty()) }) } + } - fun stopDownload(chapter: Chapter) { - scope.launch { stopChapterDownload.await(chapter.id, onError = { toast(it.message.orEmpty()) }) } - } + fun stopDownload(chapter: Chapter) { + scope.launch { stopChapterDownload.await(chapter.id, onError = { toast(it.message.orEmpty()) }) } + } - fun moveUp(chapter: Chapter) { - scope.launch { - val index = downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index } - if (index == -1 || index <= 0) return@launch - reorderChapterDownload.await(chapter.id, index - 1, onError = { toast(it.message.orEmpty()) }) - } - } - - fun moveDown(chapter: Chapter) { - scope.launch { - val index = downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index } - if (index == -1 || index >= downloadQueue.value.lastIndex) return@launch - reorderChapterDownload.await(chapter.id, index + 1, onError = { toast(it.message.orEmpty()) }) - } - } - - fun moveToTop(chapter: Chapter) { - scope.launch { - reorderChapterDownload.await(chapter.id, 0, onError = { toast(it.message.orEmpty()) }) - } - } - - fun moveToBottom(chapter: Chapter) { - scope.launch { - reorderChapterDownload.await(chapter.id, downloadQueue.value.lastIndex, onError = { toast(it.message.orEmpty()) }) - } - } - - fun restartDownloader() = startDownloadService(contextWrapper, downloadService, Actions.RESTART) - - override fun onDispose() { - super.onDispose() - uiScope?.cancel() - } - - private companion object { - private val log = logging() + fun moveUp(chapter: Chapter) { + scope.launch { + val index = + downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index } + if (index == -1 || index <= 0) return@launch + reorderChapterDownload.await(chapter.id, index - 1, onError = { toast(it.message.orEmpty()) }) } } + + fun moveDown(chapter: Chapter) { + scope.launch { + val index = + downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index } + if (index == -1 || index >= downloadQueue.value.lastIndex) return@launch + reorderChapterDownload.await(chapter.id, index + 1, onError = { toast(it.message.orEmpty()) }) + } + } + + fun moveToTop(chapter: Chapter) { + scope.launch { + reorderChapterDownload.await(chapter.id, 0, onError = { toast(it.message.orEmpty()) }) + } + } + + fun moveToBottom(chapter: Chapter) { + scope.launch { + reorderChapterDownload.await( + chapter.id, + downloadQueue.value.lastIndex, + onError = { toast(it.message.orEmpty()) }, + ) + } + } + + fun restartDownloader() = startDownloadService(contextWrapper, downloadService, Actions.RESTART) + + override fun onDispose() { + super.onDispose() + uiScope?.cancel() + } + + private companion object { + private val log = logging() + } +} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreenViewModel.kt index 52d312c9..0e028d53 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreenViewModel.kt @@ -41,188 +41,187 @@ import okio.Source import org.lighthousegames.logging.logging import kotlin.random.Random -class ExtensionsScreenViewModel - @Inject - constructor( - private val getExtensionList: GetExtensionList, - private val installExtensionFile: InstallExtensionFile, - private val installExtension: InstallExtension, - private val updateExtension: UpdateExtension, - private val uninstallExtension: UninstallExtension, - extensionPreferences: ExtensionPreferences, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - private val extensionList = MutableStateFlow?>(null) +@Inject +class ExtensionsScreenViewModel( + private val getExtensionList: GetExtensionList, + private val installExtensionFile: InstallExtensionFile, + private val installExtension: InstallExtension, + private val updateExtension: UpdateExtension, + private val uninstallExtension: UninstallExtension, + extensionPreferences: ExtensionPreferences, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + private val extensionList = MutableStateFlow?>(null) - private val _enabledLangs = extensionPreferences.languages().asStateFlow() - val enabledLangs = _enabledLangs.map { it.toImmutableSet() } - .stateIn(scope, SharingStarted.Eagerly, persistentSetOf()) + private val _enabledLangs = extensionPreferences.languages().asStateFlow() + val enabledLangs = _enabledLangs.map { it.toImmutableSet() } + .stateIn(scope, SharingStarted.Eagerly, persistentSetOf()) - private val _searchQuery = MutableStateFlow(null) - val searchQuery = _searchQuery.asStateFlow() + private val _searchQuery = MutableStateFlow(null) + val searchQuery = _searchQuery.asStateFlow() - private val workingExtensions = MutableStateFlow>(emptyList()) + private val workingExtensions = MutableStateFlow>(emptyList()) - val extensions = combine( - searchQuery, - extensionList, - enabledLangs, - workingExtensions, - ) { searchQuery, extensions, enabledLangs, workingExtensions -> - search(searchQuery, extensions, enabledLangs, workingExtensions) - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + val extensions = combine( + searchQuery, + extensionList, + enabledLangs, + workingExtensions, + ) { searchQuery, extensions, enabledLangs, workingExtensions -> + search(searchQuery, extensions, enabledLangs, workingExtensions) + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - val availableLangs = extensionList.filterNotNull().map { langs -> - langs.map { it.lang }.distinct().toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + val availableLangs = extensionList.filterNotNull().map { langs -> + langs.map { it.lang }.distinct().toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - val isLoading = extensionList.map { it == null }.stateIn(scope, SharingStarted.Eagerly, true) + val isLoading = extensionList.map { it == null }.stateIn(scope, SharingStarted.Eagerly, true) - init { - scope.launch { - getExtensions() - } - } - - private suspend fun getExtensions() { - extensionList.value = getExtensionList.await(onError = { toast(it.message.orEmpty()) }).orEmpty() - } - - fun install(source: Source) { - log.info { "Install file clicked" } - scope.launch { - try { - val file = FileSystem.SYSTEM_TEMPORARY_DIRECTORY - .resolve("tachidesk.${Random.nextLong()}.apk") - .also { file -> - source.saveTo(file) - } - installExtensionFile.await(file, onError = { toast(it.message.orEmpty()) }) - } catch (e: Exception) { - log.warn(e) { "Error creating apk file" } - // todo toast if error - e.throwIfCancellation() - } - - getExtensions() - } - } - - fun install(extension: Extension) { - log.info { "Install clicked" } - scope.launch { - workingExtensions.update { it + extension.apkName } - installExtension.await(extension, onError = { toast(it.message.orEmpty()) }) - getExtensions() - workingExtensions.update { it - extension.apkName } - } - } - - fun update(extension: Extension) { - log.info { "Update clicked" } - scope.launch { - workingExtensions.update { it + extension.apkName } - updateExtension.await(extension, onError = { toast(it.message.orEmpty()) }) - getExtensions() - workingExtensions.update { it - extension.apkName } - } - } - - fun uninstall(extension: Extension) { - log.info { "Uninstall clicked" } - scope.launch { - workingExtensions.update { it + extension.apkName } - uninstallExtension.await(extension, onError = { toast(it.message.orEmpty()) }) - getExtensions() - workingExtensions.update { it - extension.apkName } - } - } - - fun setEnabledLanguages(langs: Set) { - _enabledLangs.value = langs - } - - fun setQuery(query: String) { - _searchQuery.value = query - } - - private fun search( - searchQuery: String?, - extensionList: List?, - enabledLangs: Set, - workingExtensions: List, - ): ImmutableList { - val extensions = extensionList?.filter { it.lang in enabledLangs } - .orEmpty() - return if (searchQuery.isNullOrBlank()) { - extensions.splitSort(workingExtensions) - } else { - val queries = searchQuery.split(" ") - val filteredExtensions = extensions.toMutableList() - queries.forEach { query -> - filteredExtensions.removeAll { !it.name.contains(query, true) } - } - filteredExtensions.toList().splitSort(workingExtensions) - } - } - - private fun List.splitSort(workingExtensions: List): ImmutableList { - val all = MR.strings.all.toPlatformString() - return this - .filter(Extension::installed) - .sortedWith( - compareBy { - when { - it.obsolete -> 1 - it.hasUpdate -> 2 - else -> 3 - } - } - .thenBy(Extension::lang) - .thenBy(String.CASE_INSENSITIVE_ORDER, Extension::name), - ) - .map { ExtensionUI.ExtensionItem(it, it.apkName in workingExtensions) } - .let { - if (it.isNotEmpty()) { - listOf(ExtensionUI.Header(MR.strings.installed.toPlatformString())) + it - } else { - it - } - }.plus( - filterNot(Extension::installed) - .groupBy(Extension::lang) - .mapKeys { (key) -> - when (key) { - "all" -> all - else -> Locale(key).displayName - } - } - .mapValues { - it.value - .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Extension::name)) - .map { ExtensionUI.ExtensionItem(it, it.apkName in workingExtensions) } - } - .toList() - .sortedWith( - compareBy> { (key) -> - when (key) { - all -> 1 - else -> 2 - } - }.thenBy(String.CASE_INSENSITIVE_ORDER, Pair::first), - ) - .flatMap { (key, value) -> - listOf(ExtensionUI.Header(key)) + value - }, - ) - .toImmutableList() - } - - private companion object { - private val log = logging() + init { + scope.launch { + getExtensions() } } + private suspend fun getExtensions() { + extensionList.value = getExtensionList.await(onError = { toast(it.message.orEmpty()) }).orEmpty() + } + + fun install(source: Source) { + log.info { "Install file clicked" } + scope.launch { + try { + val file = FileSystem.SYSTEM_TEMPORARY_DIRECTORY + .resolve("tachidesk.${Random.nextLong()}.apk") + .also { file -> + source.saveTo(file) + } + installExtensionFile.await(file, onError = { toast(it.message.orEmpty()) }) + } catch (e: Exception) { + log.warn(e) { "Error creating apk file" } + // todo toast if error + e.throwIfCancellation() + } + + getExtensions() + } + } + + fun install(extension: Extension) { + log.info { "Install clicked" } + scope.launch { + workingExtensions.update { it + extension.apkName } + installExtension.await(extension, onError = { toast(it.message.orEmpty()) }) + getExtensions() + workingExtensions.update { it - extension.apkName } + } + } + + fun update(extension: Extension) { + log.info { "Update clicked" } + scope.launch { + workingExtensions.update { it + extension.apkName } + updateExtension.await(extension, onError = { toast(it.message.orEmpty()) }) + getExtensions() + workingExtensions.update { it - extension.apkName } + } + } + + fun uninstall(extension: Extension) { + log.info { "Uninstall clicked" } + scope.launch { + workingExtensions.update { it + extension.apkName } + uninstallExtension.await(extension, onError = { toast(it.message.orEmpty()) }) + getExtensions() + workingExtensions.update { it - extension.apkName } + } + } + + fun setEnabledLanguages(langs: Set) { + _enabledLangs.value = langs + } + + fun setQuery(query: String) { + _searchQuery.value = query + } + + private fun search( + searchQuery: String?, + extensionList: List?, + enabledLangs: Set, + workingExtensions: List, + ): ImmutableList { + val extensions = extensionList?.filter { it.lang in enabledLangs } + .orEmpty() + return if (searchQuery.isNullOrBlank()) { + extensions.splitSort(workingExtensions) + } else { + val queries = searchQuery.split(" ") + val filteredExtensions = extensions.toMutableList() + queries.forEach { query -> + filteredExtensions.removeAll { !it.name.contains(query, true) } + } + filteredExtensions.toList().splitSort(workingExtensions) + } + } + + private fun List.splitSort(workingExtensions: List): ImmutableList { + val all = MR.strings.all.toPlatformString() + return this + .filter(Extension::installed) + .sortedWith( + compareBy { + when { + it.obsolete -> 1 + it.hasUpdate -> 2 + else -> 3 + } + } + .thenBy(Extension::lang) + .thenBy(String.CASE_INSENSITIVE_ORDER, Extension::name), + ) + .map { ExtensionUI.ExtensionItem(it, it.apkName in workingExtensions) } + .let { + if (it.isNotEmpty()) { + listOf(ExtensionUI.Header(MR.strings.installed.toPlatformString())) + it + } else { + it + } + }.plus( + filterNot(Extension::installed) + .groupBy(Extension::lang) + .mapKeys { (key) -> + when (key) { + "all" -> all + else -> Locale(key).displayName + } + } + .mapValues { + it.value + .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Extension::name)) + .map { ExtensionUI.ExtensionItem(it, it.apkName in workingExtensions) } + } + .toList() + .sortedWith( + compareBy> { (key) -> + when (key) { + all -> 1 + else -> 2 + } + }.thenBy(String.CASE_INSENSITIVE_ORDER, Pair::first), + ) + .flatMap { (key, value) -> + listOf(ExtensionUI.Header(key)) + value + }, + ) + .toImmutableList() + } + + private companion object { + private val log = logging() + } +} + @Immutable sealed class ExtensionUI { data class Header( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt index 0b60af30..7127c64f 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt @@ -119,224 +119,223 @@ private fun LibraryMap.setManga( } } -class LibraryScreenViewModel - @Inject - constructor( - private val getCategories: GetCategories, - private val getMangaListFromCategory: GetMangaListFromCategory, - private val removeMangaFromLibrary: RemoveMangaFromLibrary, - private val updateLibrary: UpdateLibrary, - private val updateCategory: UpdateCategory, - libraryPreferences: LibraryPreferences, - contextWrapper: ContextWrapper, - @Assisted private val savedStateHandle: SavedStateHandle, - ) : ViewModel(contextWrapper) { - private val library = Library(MutableStateFlow(LibraryState.Loading), mutableMapOf()) - val categories = library.categories.asStateFlow() +@Inject +class LibraryScreenViewModel( + private val getCategories: GetCategories, + private val getMangaListFromCategory: GetMangaListFromCategory, + private val removeMangaFromLibrary: RemoveMangaFromLibrary, + private val updateLibrary: UpdateLibrary, + private val updateCategory: UpdateCategory, + libraryPreferences: LibraryPreferences, + contextWrapper: ContextWrapper, + @Assisted private val savedStateHandle: SavedStateHandle, +) : ViewModel(contextWrapper) { + private val library = Library(MutableStateFlow(LibraryState.Loading), mutableMapOf()) + val categories = library.categories.asStateFlow() - private val _selectedCategoryIndex by savedStateHandle.getStateFlow { 0 } - val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow() + private val _selectedCategoryIndex by savedStateHandle.getStateFlow { 0 } + val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow() - private val _showingMenu by savedStateHandle.getStateFlow { false } - val showingMenu = _showingMenu.asStateFlow() + private val _showingMenu by savedStateHandle.getStateFlow { false } + val showingMenu = _showingMenu.asStateFlow() - val displayMode = libraryPreferences.displayMode().stateIn(scope) - val gridColumns = libraryPreferences.gridColumns().stateIn(scope) - val gridSize = libraryPreferences.gridSize().stateIn(scope) - val unreadBadges = libraryPreferences.unreadBadge().stateIn(scope) - val downloadBadges = libraryPreferences.downloadBadge().stateIn(scope) - val languageBadges = libraryPreferences.languageBadge().stateIn(scope) - val localBadges = libraryPreferences.localBadge().stateIn(scope) + val displayMode = libraryPreferences.displayMode().stateIn(scope) + val gridColumns = libraryPreferences.gridColumns().stateIn(scope) + val gridSize = libraryPreferences.gridSize().stateIn(scope) + val unreadBadges = libraryPreferences.unreadBadge().stateIn(scope) + val downloadBadges = libraryPreferences.downloadBadge().stateIn(scope) + val languageBadges = libraryPreferences.languageBadge().stateIn(scope) + val localBadges = libraryPreferences.localBadge().stateIn(scope) - private val sortMode = libraryPreferences.sortMode().stateIn(scope) - private val sortAscending = libraryPreferences.sortAscending().stateIn(scope) + private val sortMode = libraryPreferences.sortMode().stateIn(scope) + private val sortAscending = libraryPreferences.sortAscending().stateIn(scope) - private val filter: Flow<(Manga) -> Boolean> = combine( - libraryPreferences.filterDownloaded().getAsFlow(), - libraryPreferences.filterUnread().getAsFlow(), - libraryPreferences.filterCompleted().getAsFlow(), - ) { downloaded, unread, completed -> - { manga -> - when (downloaded) { - FilterState.EXCLUDED -> manga.downloadCount == null || manga.downloadCount == 0 - FilterState.INCLUDED -> manga.downloadCount != null && (manga.downloadCount ?: 0) > 0 + private val filter: Flow<(Manga) -> Boolean> = combine( + libraryPreferences.filterDownloaded().getAsFlow(), + libraryPreferences.filterUnread().getAsFlow(), + libraryPreferences.filterCompleted().getAsFlow(), + ) { downloaded, unread, completed -> + { manga -> + when (downloaded) { + FilterState.EXCLUDED -> manga.downloadCount == null || manga.downloadCount == 0 + FilterState.INCLUDED -> manga.downloadCount != null && (manga.downloadCount ?: 0) > 0 + FilterState.IGNORED -> true + } && + when (unread) { + FilterState.EXCLUDED -> manga.unreadCount == null || manga.unreadCount == 0 + FilterState.INCLUDED -> manga.unreadCount != null && (manga.unreadCount ?: 0) > 0 FilterState.IGNORED -> true } && - when (unread) { - FilterState.EXCLUDED -> manga.unreadCount == null || manga.unreadCount == 0 - FilterState.INCLUDED -> manga.unreadCount != null && (manga.unreadCount ?: 0) > 0 - FilterState.IGNORED -> true - } && - when (completed) { - FilterState.EXCLUDED -> manga.status != MangaStatus.COMPLETED - FilterState.INCLUDED -> manga.status == MangaStatus.COMPLETED - FilterState.IGNORED -> true - } - } - } - - private val _query by savedStateHandle.getStateFlow { "" } - val query = _query.asStateFlow() - - private val comparator = combine(sortMode, sortAscending) { sortMode, sortAscending -> - getComparator(sortMode, sortAscending) - }.stateIn(scope, SharingStarted.Eagerly, compareBy { it.title }) - - init { - getLibrary() - } - - private fun getLibrary() { - library.categories.value = LibraryState.Loading - getCategories.asFlow() - .onEach { categories -> - if (categories.isEmpty()) { - throw Exception(MR.strings.library_empty.toPlatformString()) - } - library.categories.value = LibraryState.Loaded( - categories.sortedBy { it.order } - .toImmutableList(), - ) - categories.forEach { category -> - getMangaListFromCategory.asFlow(category) - .onEach { - library.mangaMap.setManga( - id = category.id, - manga = it.toImmutableList(), - getItemsFlow = ::getMangaItemsFlow, - ) - } - .catch { - log.warn(it) { "Failed to get manga list from category ${category.name}" } - library.mangaMap.setError(category.id, it) - } - .launchIn(scope) - } + when (completed) { + FilterState.EXCLUDED -> manga.status != MangaStatus.COMPLETED + FilterState.INCLUDED -> manga.status == MangaStatus.COMPLETED + FilterState.IGNORED -> true } - .catch { - library.categories.value = LibraryState.Failed(it) - log.warn(it) { "Failed to get categories" } - } - .launchIn(scope) - } - - fun setSelectedPage(page: Int) { - _selectedCategoryIndex.value = page - } - - fun setShowingMenu(showingMenu: Boolean) { - _showingMenu.value = showingMenu - } - - private fun getComparator( - sortMode: Sort, - ascending: Boolean, - ): Comparator { - val sortFn: (Manga, Manga) -> Int = when (sortMode) { - Sort.ALPHABETICAL -> { - val locale = Locale.current - val collator = CollatorComparator(locale); - - { a, b -> - collator.compare(a.title.toLowerCase(locale), b.title.toLowerCase(locale)) - } - } - - Sort.UNREAD -> { - { a, b -> - when { - // Ensure unread content comes first - (a.unreadCount ?: 0) == (b.unreadCount ?: 0) -> 0 - - a.unreadCount == null || a.unreadCount == 0 -> if (ascending) 1 else -1 - - b.unreadCount == null || b.unreadCount == 0 -> if (ascending) -1 else 1 - - else -> (a.unreadCount ?: 0).compareTo(b.unreadCount ?: 0) - } - } - } - - Sort.DATE_ADDED -> { - { a, b -> - a.inLibraryAt.compareTo(b.inLibraryAt) - } - } - } - return if (ascending) { - Comparator(sortFn) - } else { - Comparator(sortFn).reversed() - } - } - - private suspend fun filterManga( - query: String, - mangaList: List, - ): List { - if (query.isBlank()) return mangaList - val queries = query.split(" ") - return mangaList.asFlow() - .filter { manga -> - queries.all { query -> - manga.title.contains(query, true) || - manga.author.orEmpty().contains(query, true) || - manga.artist.orEmpty().contains(query, true) || - manga.genre.any { it.contains(query, true) } || - manga.description.orEmpty().contains(query, true) || - manga.status.name.contains(query, true) - } - } - .cancellable() - .buffer() - .toList() - } - - private fun getMangaItemsFlow(unfilteredItemsFlow: StateFlow>): StateFlow> = - combine( - unfilteredItemsFlow, - query, - ) { - unfilteredItems, - query, - -> - filterManga(query, unfilteredItems) - }.combine(filter) { filteredManga, filterer -> - filteredManga.filter(filterer) - }.combine(comparator) { filteredManga, comparator -> - filteredManga.sortedWith(comparator) - }.map { - it.toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - - fun getLibraryForCategoryId(id: Long): StateFlow = library.mangaMap.getManga(id) - - private fun getCategoriesToUpdate(mangaId: Long): List = - library.mangaMap - .filter { mangaMapEntry -> - (mangaMapEntry.value.value as? CategoryState.Loaded)?.items?.value?.firstOrNull { it.id == mangaId } != null - } - .mapNotNull { (id) -> (library.categories.value as? LibraryState.Loaded)?.categories?.first { it.id == id } } - - fun removeManga(mangaId: Long) { - scope.launch { - removeMangaFromLibrary.await(mangaId, onError = { toast(it.message.orEmpty()) }) - } - } - - fun updateQuery(query: String) { - _query.value = query - } - - fun updateLibrary() { - scope.launch { updateLibrary.await(onError = { toast(it.message.orEmpty()) }) } - } - - fun updateCategory(category: Category) { - scope.launch { updateCategory.await(category, onError = { toast(it.message.orEmpty()) }) } - } - - private companion object { - private val log = logging() } } + + private val _query by savedStateHandle.getStateFlow { "" } + val query = _query.asStateFlow() + + private val comparator = combine(sortMode, sortAscending) { sortMode, sortAscending -> + getComparator(sortMode, sortAscending) + }.stateIn(scope, SharingStarted.Eagerly, compareBy { it.title }) + + init { + getLibrary() + } + + private fun getLibrary() { + library.categories.value = LibraryState.Loading + getCategories.asFlow() + .onEach { categories -> + if (categories.isEmpty()) { + throw Exception(MR.strings.library_empty.toPlatformString()) + } + library.categories.value = LibraryState.Loaded( + categories.sortedBy { it.order } + .toImmutableList(), + ) + categories.forEach { category -> + getMangaListFromCategory.asFlow(category) + .onEach { + library.mangaMap.setManga( + id = category.id, + manga = it.toImmutableList(), + getItemsFlow = ::getMangaItemsFlow, + ) + } + .catch { + log.warn(it) { "Failed to get manga list from category ${category.name}" } + library.mangaMap.setError(category.id, it) + } + .launchIn(scope) + } + } + .catch { + library.categories.value = LibraryState.Failed(it) + log.warn(it) { "Failed to get categories" } + } + .launchIn(scope) + } + + fun setSelectedPage(page: Int) { + _selectedCategoryIndex.value = page + } + + fun setShowingMenu(showingMenu: Boolean) { + _showingMenu.value = showingMenu + } + + private fun getComparator( + sortMode: Sort, + ascending: Boolean, + ): Comparator { + val sortFn: (Manga, Manga) -> Int = when (sortMode) { + Sort.ALPHABETICAL -> { + val locale = Locale.current + val collator = CollatorComparator(locale); + + { a, b -> + collator.compare(a.title.toLowerCase(locale), b.title.toLowerCase(locale)) + } + } + + Sort.UNREAD -> { + { a, b -> + when { + // Ensure unread content comes first + (a.unreadCount ?: 0) == (b.unreadCount ?: 0) -> 0 + + a.unreadCount == null || a.unreadCount == 0 -> if (ascending) 1 else -1 + + b.unreadCount == null || b.unreadCount == 0 -> if (ascending) -1 else 1 + + else -> (a.unreadCount ?: 0).compareTo(b.unreadCount ?: 0) + } + } + } + + Sort.DATE_ADDED -> { + { a, b -> + a.inLibraryAt.compareTo(b.inLibraryAt) + } + } + } + return if (ascending) { + Comparator(sortFn) + } else { + Comparator(sortFn).reversed() + } + } + + private suspend fun filterManga( + query: String, + mangaList: List, + ): List { + if (query.isBlank()) return mangaList + val queries = query.split(" ") + return mangaList.asFlow() + .filter { manga -> + queries.all { query -> + manga.title.contains(query, true) || + manga.author.orEmpty().contains(query, true) || + manga.artist.orEmpty().contains(query, true) || + manga.genre.any { it.contains(query, true) } || + manga.description.orEmpty().contains(query, true) || + manga.status.name.contains(query, true) + } + } + .cancellable() + .buffer() + .toList() + } + + private fun getMangaItemsFlow(unfilteredItemsFlow: StateFlow>): StateFlow> = + combine( + unfilteredItemsFlow, + query, + ) { + unfilteredItems, + query, + -> + filterManga(query, unfilteredItems) + }.combine(filter) { filteredManga, filterer -> + filteredManga.filter(filterer) + }.combine(comparator) { filteredManga, comparator -> + filteredManga.sortedWith(comparator) + }.map { + it.toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + + fun getLibraryForCategoryId(id: Long): StateFlow = library.mangaMap.getManga(id) + + private fun getCategoriesToUpdate(mangaId: Long): List = + library.mangaMap + .filter { mangaMapEntry -> + (mangaMapEntry.value.value as? CategoryState.Loaded)?.items?.value?.firstOrNull { it.id == mangaId } != null + } + .mapNotNull { (id) -> (library.categories.value as? LibraryState.Loaded)?.categories?.first { it.id == id } } + + fun removeManga(mangaId: Long) { + scope.launch { + removeMangaFromLibrary.await(mangaId, onError = { toast(it.message.orEmpty()) }) + } + } + + fun updateQuery(query: String) { + _query.value = query + } + + fun updateLibrary() { + scope.launch { updateLibrary.await(onError = { toast(it.message.orEmpty()) }) } + } + + fun updateCategory(category: Category) { + scope.launch { updateCategory.await(category, onError = { toast(it.message.orEmpty()) }) } + } + + private companion object { + private val log = logging() + } +} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySettingsViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySettingsViewModel.kt index d4e7f8b2..ded42c27 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySettingsViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySettingsViewModel.kt @@ -11,22 +11,21 @@ import ca.gosyer.jui.uicore.vm.ContextWrapper import ca.gosyer.jui.uicore.vm.ViewModel import me.tatarka.inject.annotations.Inject -class LibrarySettingsViewModel - @Inject - constructor( - libraryPreferences: LibraryPreferences, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - val filterDownloaded = libraryPreferences.filterDownloaded().asStateFlow() - val filterUnread = libraryPreferences.filterUnread().asStateFlow() - val filterCompleted = libraryPreferences.filterCompleted().asStateFlow() +@Inject +class LibrarySettingsViewModel( + libraryPreferences: LibraryPreferences, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + val filterDownloaded = libraryPreferences.filterDownloaded().asStateFlow() + val filterUnread = libraryPreferences.filterUnread().asStateFlow() + val filterCompleted = libraryPreferences.filterCompleted().asStateFlow() - val sortMode = libraryPreferences.sortMode().asStateFlow() - val sortAscending = libraryPreferences.sortAscending().asStateFlow() + val sortMode = libraryPreferences.sortMode().asStateFlow() + val sortAscending = libraryPreferences.sortAscending().asStateFlow() - val displayMode = libraryPreferences.displayMode().asStateFlow() - val unreadBadges = libraryPreferences.unreadBadge().asStateFlow() - val downloadBadges = libraryPreferences.downloadBadge().asStateFlow() - val languageBadges = libraryPreferences.languageBadge().asStateFlow() - val localBadges = libraryPreferences.localBadge().asStateFlow() - } + val displayMode = libraryPreferences.displayMode().asStateFlow() + val unreadBadges = libraryPreferences.unreadBadge().asStateFlow() + val downloadBadges = libraryPreferences.downloadBadge().asStateFlow() + val languageBadges = libraryPreferences.languageBadge().asStateFlow() + val localBadges = libraryPreferences.localBadge().asStateFlow() +} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainViewModel.kt index 736d63d6..67753551 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainViewModel.kt @@ -14,23 +14,22 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import me.tatarka.inject.annotations.Inject -class MainViewModel - @Inject - constructor( - uiPreferences: UiPreferences, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - override val scope = MainScope() +@Inject +class MainViewModel( + uiPreferences: UiPreferences, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + override val scope = MainScope() - val startScreen = uiPreferences.startScreen().get() - val confirmExit = uiPreferences.confirmExit().stateIn(scope) + val startScreen = uiPreferences.startScreen().get() + val confirmExit = uiPreferences.confirmExit().stateIn(scope) - override fun onDispose() { - super.onDispose() - scope.cancel() - } - - fun confirmExitToast() { - toast(MR.strings.confirm_exit_toast.toPlatformString()) - } + override fun onDispose() { + super.onDispose() + scope.cancel() } + + fun confirmExitToast() { + toast(MR.strings.confirm_exit_toast.toPlatformString()) + } +} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/AboutViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/AboutViewModel.kt index ede2d5d8..9f0953f6 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/AboutViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/AboutViewModel.kt @@ -26,49 +26,48 @@ import kotlinx.datetime.Instant import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class AboutViewModel - @Inject - constructor( - private val dateHandler: DateHandler, - private val aboutServer: AboutServer, - private val updateChecker: UpdateChecker, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - private val _aboutHolder = MutableStateFlow(null) - val aboutHolder = _aboutHolder.asStateFlow() +@Inject +class AboutViewModel( + private val dateHandler: DateHandler, + private val aboutServer: AboutServer, + private val updateChecker: UpdateChecker, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + private val _aboutHolder = MutableStateFlow(null) + val aboutHolder = _aboutHolder.asStateFlow() - val formattedBuildTime = aboutHolder.map { about -> - about ?: return@map "" - getFormattedDate(Instant.fromEpochSeconds(about.buildTime)) - }.stateIn(scope, SharingStarted.Eagerly, "") + val formattedBuildTime = aboutHolder.map { about -> + about ?: return@map "" + getFormattedDate(Instant.fromEpochSeconds(about.buildTime)) + }.stateIn(scope, SharingStarted.Eagerly, "") - private val _updates = MutableSharedFlow() - val updates = _updates.asSharedFlow() + private val _updates = MutableSharedFlow() + val updates = _updates.asSharedFlow() - init { - getAbout() - } + init { + getAbout() + } - private fun getAbout() { - scope.launch { - _aboutHolder.value = aboutServer.await(onError = { toast(it.message.orEmpty()) }) - } - } - - fun checkForUpdates() { - scope.launch { - toast(MR.strings.update_check_look_for_updates.toPlatformString()) - when (val update = updateChecker.await(true, onError = { toast(it.message.orEmpty()) })) { - is Update.UpdateFound -> _updates.emit(update) - is Update.NoUpdatesFound -> toast(MR.strings.update_check_no_new_updates.toPlatformString()) - null -> Unit - } - } - } - - private fun getFormattedDate(time: Instant): String = dateHandler.dateTimeFormat(time) - - companion object { - private val log = logging() + private fun getAbout() { + scope.launch { + _aboutHolder.value = aboutServer.await(onError = { toast(it.message.orEmpty()) }) } } + + fun checkForUpdates() { + scope.launch { + toast(MR.strings.update_check_look_for_updates.toPlatformString()) + when (val update = updateChecker.await(true, onError = { toast(it.message.orEmpty()) })) { + is Update.UpdateFound -> _updates.emit(update) + is Update.NoUpdatesFound -> toast(MR.strings.update_check_no_new_updates.toPlatformString()) + null -> Unit + } + } + } + + private fun getFormattedDate(time: Instant): String = dateHandler.dateTimeFormat(time) + + companion object { + private val log = logging() + } +} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/LibraryUpdatesViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/LibraryUpdatesViewModel.kt index 3a31d654..d636d344 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/LibraryUpdatesViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/LibraryUpdatesViewModel.kt @@ -18,33 +18,32 @@ import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class LibraryUpdatesViewModel - @Inject - constructor( - private val libraryUpdateService: LibraryUpdateService, - private val contextWrapper: ContextWrapper, - @Assisted standalone: Boolean, - ) : ViewModel(contextWrapper) { - private val uiScope = if (standalone) { - MainScope() - } else { - null - } - - override val scope: CoroutineScope - get() = uiScope ?: super.scope - - val serviceStatus = LibraryUpdateService.status.asStateFlow() - val updateStatus = LibraryUpdateService.updateStatus.asStateFlow() - - fun restartLibraryUpdates() = startLibraryUpdatesService(contextWrapper, libraryUpdateService, Actions.RESTART) - - override fun onDispose() { - super.onDispose() - uiScope?.cancel() - } - - private companion object { - private val log = logging() - } +@Inject +class LibraryUpdatesViewModel( + private val libraryUpdateService: LibraryUpdateService, + private val contextWrapper: ContextWrapper, + @Assisted standalone: Boolean, +) : ViewModel(contextWrapper) { + private val uiScope = if (standalone) { + MainScope() + } else { + null } + + override val scope: CoroutineScope + get() = uiScope ?: super.scope + + val serviceStatus = LibraryUpdateService.status.asStateFlow() + val updateStatus = LibraryUpdateService.updateStatus.asStateFlow() + + fun restartLibraryUpdates() = startLibraryUpdatesService(contextWrapper, libraryUpdateService, Actions.RESTART) + + override fun onDispose() { + super.onDispose() + uiScope?.cancel() + } + + private companion object { + private val log = logging() + } +} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt index b7bb5e78..d623bd3e 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt @@ -55,386 +55,386 @@ import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class MangaScreenViewModel - @Inject - constructor( - private val dateHandler: DateHandler, - private val getManga: GetManga, - private val refreshManga: RefreshManga, - private val getChapters: GetChapters, - private val refreshChapters: RefreshChapters, - private val updateChapter: UpdateChapter, - private val queueChapterDownload: QueueChapterDownload, - private val stopChapterDownload: StopChapterDownload, - private val deleteChapterDownload: DeleteChapterDownload, - private val getCategories: GetCategories, - private val getMangaCategories: GetMangaCategories, - private val addMangaToCategory: AddMangaToCategory, - private val removeMangaFromCategory: RemoveMangaFromCategory, - private val addMangaToLibrary: AddMangaToLibrary, - private val removeMangaFromLibrary: RemoveMangaFromLibrary, - private val batchChapterDownload: BatchChapterDownload, - uiPreferences: UiPreferences, - contextWrapper: ContextWrapper, - @Assisted private val params: Params, - ) : ViewModel(contextWrapper) { - private val _manga = MutableStateFlow(null) - val manga = _manga.asStateFlow() +@Inject +class MangaScreenViewModel( + private val dateHandler: DateHandler, + private val getManga: GetManga, + private val refreshManga: RefreshManga, + private val getChapters: GetChapters, + private val refreshChapters: RefreshChapters, + private val updateChapter: UpdateChapter, + private val queueChapterDownload: QueueChapterDownload, + private val stopChapterDownload: StopChapterDownload, + private val deleteChapterDownload: DeleteChapterDownload, + private val getCategories: GetCategories, + private val getMangaCategories: GetMangaCategories, + private val addMangaToCategory: AddMangaToCategory, + private val removeMangaFromCategory: RemoveMangaFromCategory, + private val addMangaToLibrary: AddMangaToLibrary, + private val removeMangaFromLibrary: RemoveMangaFromLibrary, + private val batchChapterDownload: BatchChapterDownload, + uiPreferences: UiPreferences, + contextWrapper: ContextWrapper, + @Assisted private val params: Params, +) : ViewModel(contextWrapper) { + private val _manga = MutableStateFlow(null) + val manga = _manga.asStateFlow() - private val _chapters = MutableStateFlow>(persistentListOf()) - val chapters = _chapters.asStateFlow() + private val _chapters = MutableStateFlow>(persistentListOf()) + val chapters = _chapters.asStateFlow() - private val _selectedIds = MutableStateFlow>(persistentListOf()) - val selectedItems = combine(chapters, _selectedIds) { chapters, selecteditems -> - chapters.filter { it.isSelected(selecteditems) }.toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + private val _selectedIds = MutableStateFlow>(persistentListOf()) + val selectedItems = combine(chapters, _selectedIds) { chapters, selecteditems -> + chapters.filter { it.isSelected(selecteditems) }.toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - private val loadingManga = MutableStateFlow(true) - private val loadingChapters = MutableStateFlow(true) - private val refreshingChapters = MutableStateFlow(false) - private val refreshingManga = MutableStateFlow(false) - val isLoading = combine(loadingManga, loadingChapters, refreshingManga, refreshingChapters) { a, b, c, d -> a || b || c || d } + private val loadingManga = MutableStateFlow(true) + private val loadingChapters = MutableStateFlow(true) + private val refreshingChapters = MutableStateFlow(false) + private val refreshingManga = MutableStateFlow(false) + val isLoading = + combine(loadingManga, loadingChapters, refreshingManga, refreshingChapters) { a, b, c, d -> a || b || c || d } .stateIn(scope, SharingStarted.Eagerly, true) - val categories = getCategories.asFlow(true) - .map { it.toImmutableList() } + val categories = getCategories.asFlow(true) + .map { it.toImmutableList() } + .catch { + toast(it.message.orEmpty()) + log.warn(it) { "Failed to get categories" } + } + .stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + + private val _mangaCategories = MutableStateFlow>(persistentListOf()) + val mangaCategories = _mangaCategories.asStateFlow() + + val categoriesExist = categories.map { it.isNotEmpty() } + .stateIn(scope, SharingStarted.Eagerly, true) + + val inActionMode = _selectedIds.map { it.isNotEmpty() } + .stateIn(scope, SharingStarted.Eagerly, false) + + private val chooseCategoriesFlow = MutableSharedFlow() + val chooseCategoriesFlowHolder = StableHolder(chooseCategoriesFlow.asSharedFlow()) + + private val reloadManga = MutableSharedFlow() + private val reloadChapters = MutableSharedFlow() + + val dateTimeFormatter = uiPreferences.dateFormat().changes() + .map { + dateHandler.getDateFormat(it) + } + .asStateFlow(dateHandler.getDateFormat(uiPreferences.dateFormat().get())) + + init { + DownloadService.registerWatch(params.mangaId) + .mapLatest { downloadingChapters -> + chapters.value.forEach { chapter -> + chapter.updateFrom(downloadingChapters) + } + } + .launchIn(scope) + + reloadManga.onStart { emit(Unit) }.flatMapLatest { + loadingManga.value = true + getManga.asFlow(params.mangaId) + } + .onEach { + _manga.value = it + loadingManga.value = false + } .catch { toast(it.message.orEmpty()) - log.warn(it) { "Failed to get categories" } + log.warn(it) { "Error when loading manga" } + loadingManga.value = false } - .stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + .launchIn(scope) - private val _mangaCategories = MutableStateFlow>(persistentListOf()) - val mangaCategories = _mangaCategories.asStateFlow() - - val categoriesExist = categories.map { it.isNotEmpty() } - .stateIn(scope, SharingStarted.Eagerly, true) - - val inActionMode = _selectedIds.map { it.isNotEmpty() } - .stateIn(scope, SharingStarted.Eagerly, false) - - private val chooseCategoriesFlow = MutableSharedFlow() - val chooseCategoriesFlowHolder = StableHolder(chooseCategoriesFlow.asSharedFlow()) - - private val reloadManga = MutableSharedFlow() - private val reloadChapters = MutableSharedFlow() - - val dateTimeFormatter = uiPreferences.dateFormat().changes() - .map { - dateHandler.getDateFormat(it) + reloadChapters.onStart { emit(Unit) }.flatMapLatest { + loadingChapters.value = true + getChapters.asFlow(params.mangaId) + } + .onEach { + updateChapters(it) + loadingChapters.value = false } - .asStateFlow(dateHandler.getDateFormat(uiPreferences.dateFormat().get())) + .catch { + toast(it.message.orEmpty()) + log.warn(it) { "Error when getting chapters" } + loadingChapters.value = false + } + .launchIn(scope) - init { - DownloadService.registerWatch(params.mangaId) - .mapLatest { downloadingChapters -> - chapters.value.forEach { chapter -> - chapter.updateFrom(downloadingChapters) + scope.launch { + val mangaCategories = getMangaCategories.await(params.mangaId, onError = { toast(it.message.orEmpty()) }) + if (mangaCategories != null) { + _mangaCategories.value = mangaCategories.toImmutableList() + } + } + + scope.launch { + val manga = manga.first { it != null }!! + if (!manga.initialized) { + refreshManga() + } + } + } + + fun loadManga() { + scope.launch { + reloadManga.emit(Unit) + } + } + + fun loadChapters() { + scope.launch { + reloadChapters.emit(Unit) + } + } + + fun updateChapters(chapters: List) { + _chapters.value = chapters.sortedByDescending { it.index }.toDownloadChapters() + } + + fun refreshManga() { + scope.launch { + refreshingManga.value = true + val manga = refreshManga.await( + params.mangaId, + onError = { + log.warn(it) { "Error when refreshing manga" } + toast(it.message.orEmpty()) + }, + ) + if (manga != null) { + _manga.value = manga + } + refreshingManga.value = false + } + scope.launch { + refreshingChapters.value = true + val chapters = refreshChapters.await( + params.mangaId, + onError = { + log.warn(it) { "Error when refreshing chapters" } + toast(it.message.orEmpty()) + }, + ) + if (!chapters.isNullOrEmpty()) { + updateChapters(chapters) + } + refreshingChapters.value = false + } + } + + fun setCategories() { + scope.launch { + manga.value ?: return@launch + chooseCategoriesFlow.emit(Unit) + } + } + + fun toggleFavorite() { + scope.launch { + manga.value?.let { manga -> + if (manga.inLibrary) { + removeMangaFromLibrary.await(manga, onError = { toast(it.message.orEmpty()) }) + } else { + if (categories.value.isEmpty()) { + addFavorite(emptyList(), emptyList()) + } else { + chooseCategoriesFlow.emit(Unit) } } - .launchIn(scope) - - reloadManga.onStart { emit(Unit) }.flatMapLatest { - loadingManga.value = true - getManga.asFlow(params.mangaId) + loadManga() } - .onEach { - _manga.value = it - loadingManga.value = false - } - .catch { - toast(it.message.orEmpty()) - log.warn(it) { "Error when loading manga" } - loadingManga.value = false - } - .launchIn(scope) + } + } - reloadChapters.onStart { emit(Unit) }.flatMapLatest { - loadingChapters.value = true - getChapters.asFlow(params.mangaId) - } - .onEach { - updateChapters(it) - loadingChapters.value = false + fun addFavorite( + categories: List, + oldCategories: List, + ) { + scope.launch { + manga.value?.let { manga -> + if (manga.inLibrary) { + if (oldCategories.isEmpty()) { + removeMangaFromCategory.await(manga.id, 0, onError = { toast(it.message.orEmpty()) }) + } else { + oldCategories.filterNot { it in categories }.forEach { + removeMangaFromCategory.await(manga, it, onError = { toast(it.message.orEmpty()) }) + } + } + } else { + addMangaToLibrary.await(manga, onError = { toast(it.message.orEmpty()) }) } - .catch { - toast(it.message.orEmpty()) - log.warn(it) { "Error when getting chapters" } - loadingChapters.value = false + if (categories.isEmpty()) { + addMangaToCategory.await(manga.id, 0, onError = { toast(it.message.orEmpty()) }) + } else { + categories.filterNot { it in oldCategories }.forEach { + addMangaToCategory.await(manga, it, onError = { toast(it.message.orEmpty()) }) + } } - .launchIn(scope) - scope.launch { - val mangaCategories = getMangaCategories.await(params.mangaId, onError = { toast(it.message.orEmpty()) }) + val mangaCategories = getMangaCategories.await(manga.id, onError = { toast(it.message.orEmpty()) }) if (mangaCategories != null) { _mangaCategories.value = mangaCategories.toImmutableList() } + + loadManga() } - - scope.launch { - val manga = manga.first { it != null }!! - if (!manga.initialized) { - refreshManga() - } - } - } - - fun loadManga() { - scope.launch { - reloadManga.emit(Unit) - } - } - - fun loadChapters() { - scope.launch { - reloadChapters.emit(Unit) - } - } - - fun updateChapters(chapters: List) { - _chapters.value = chapters.sortedByDescending { it.index }.toDownloadChapters() - } - - fun refreshManga() { - scope.launch { - refreshingManga.value = true - val manga = refreshManga.await( - params.mangaId, - onError = { - log.warn(it) { "Error when refreshing manga" } - toast(it.message.orEmpty()) - } - ) - if (manga != null) { - _manga.value = manga - } - refreshingManga.value = false - } - scope.launch { - refreshingChapters.value = true - val chapters = refreshChapters.await( - params.mangaId, - onError = { - log.warn(it) { "Error when refreshing chapters" } - toast(it.message.orEmpty()) - } - ) - if (!chapters.isNullOrEmpty()) { - updateChapters(chapters) - } - refreshingChapters.value = false - } - } - - fun setCategories() { - scope.launch { - manga.value ?: return@launch - chooseCategoriesFlow.emit(Unit) - } - } - - fun toggleFavorite() { - scope.launch { - manga.value?.let { manga -> - if (manga.inLibrary) { - removeMangaFromLibrary.await(manga, onError = { toast(it.message.orEmpty()) }) - } else { - if (categories.value.isEmpty()) { - addFavorite(emptyList(), emptyList()) - } else { - chooseCategoriesFlow.emit(Unit) - } - } - loadManga() - } - } - } - - fun addFavorite( - categories: List, - oldCategories: List, - ) { - scope.launch { - manga.value?.let { manga -> - if (manga.inLibrary) { - if (oldCategories.isEmpty()) { - removeMangaFromCategory.await(manga.id, 0, onError = { toast(it.message.orEmpty()) }) - } else { - oldCategories.filterNot { it in categories }.forEach { - removeMangaFromCategory.await(manga, it, onError = { toast(it.message.orEmpty()) }) - } - } - } else { - addMangaToLibrary.await(manga, onError = { toast(it.message.orEmpty()) }) - } - if (categories.isEmpty()) { - addMangaToCategory.await(manga.id, 0, onError = { toast(it.message.orEmpty()) }) - } else { - categories.filterNot { it in oldCategories }.forEach { - addMangaToCategory.await(manga, it, onError = { toast(it.message.orEmpty()) }) - } - } - - val mangaCategories = getMangaCategories.await(manga.id, onError = { toast(it.message.orEmpty()) }) - if (mangaCategories != null) { - _mangaCategories.value = mangaCategories.toImmutableList() - } - - loadManga() - } - } - } - - private fun setRead( - chapterIds: List, - read: Boolean, - ) { - scope.launch { - manga.value?.let { - updateChapter.await(chapterIds, read = read, onError = { toast(it.message.orEmpty()) }) - _selectedIds.value = persistentListOf() - loadChapters() - } - } - } - - fun markRead(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) - - fun markUnread(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) - - private fun setBookmarked( - chapterIds: List, - bookmark: Boolean, - ) { - scope.launch { - manga.value?.let { - updateChapter.await(chapterIds, bookmarked = bookmark, onError = { toast(it.message.orEmpty()) }) - _selectedIds.value = persistentListOf() - loadChapters() - } - } - } - - fun bookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) - - fun unBookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) - - fun markPreviousRead(index: Int) { - scope.launch { - manga.value?.let { - val chapters = chapters.value - .sortedBy { it.chapter.index } - .subList(0, index).map{it.chapter.id} // todo test - updateChapter.await(chapters, read = true, onError = { toast(it.message.orEmpty()) }) - _selectedIds.value = persistentListOf() - loadChapters() - } - } - } - - fun downloadChapter(chapterId: Long) { - scope.launch { queueChapterDownload.await(chapterId, onError = { toast(it.message.orEmpty()) }) } - } - - fun deleteDownload(id: Long?) { - scope.launch { - if (id == null) { - val chapterIds = _selectedIds.value - deleteChapterDownload.await(chapterIds, onError = { toast(it.message.orEmpty()) }) - selectedItems.value.forEach { - it.setNotDownloaded() - } - _selectedIds.value = persistentListOf() - } else { - chapters.value.find { it.chapter.id == id } - ?.deleteDownload(deleteChapterDownload) - } - } - } - - fun stopDownloadingChapter(chapterId: Long) { - scope.launch { - chapters.value.find { it.chapter.id == chapterId } - ?.stopDownloading(stopChapterDownload) - } - } - - fun selectAll() { - scope.launch { - _selectedIds.value = chapters.value.map { it.chapter.id }.toImmutableList() - } - } - - fun invertSelection() { - scope.launch { - _selectedIds.value = chapters.value.map { it.chapter.id }.minus(_selectedIds.value).toImmutableList() - } - } - - fun selectChapter(id: Long) { - scope.launch { - _selectedIds.value = _selectedIds.value.plus(id).toImmutableList() - } - } - - fun unselectChapter(id: Long) { - scope.launch { - _selectedIds.value = _selectedIds.value.minus(id).toImmutableList() - } - } - - fun clearSelection() { - scope.launch { - _selectedIds.value = persistentListOf() - } - } - - fun downloadChapters() { - scope.launch { - batchChapterDownload.await(_selectedIds.value) - _selectedIds.value = persistentListOf() - } - } - - fun downloadNext(next: Int) { - scope.launch { - batchChapterDownload.await( - _chapters.value.filter { !it.chapter.read && it.downloadState.value == ChapterDownloadState.NotDownloaded } - .map { it.chapter.id } - .takeLast(next), - ) - } - } - - fun downloadUnread() { - scope.launch { - batchChapterDownload.await( - _chapters.value.filter { !it.chapter.read && it.downloadState.value == ChapterDownloadState.NotDownloaded } - .map { it.chapter.id }, - ) - } - } - - fun downloadAll() { - scope.launch { - batchChapterDownload.await( - _chapters.value - .filter { it.downloadState.value == ChapterDownloadState.NotDownloaded } - .map { it.chapter.id }, - ) - } - } - - private fun List.toDownloadChapters() = - map { - ChapterDownloadItem(null, it) - }.toImmutableList() - - data class Params( - val mangaId: Long, - ) - - private companion object { - private val log = logging() } } + + private fun setRead( + chapterIds: List, + read: Boolean, + ) { + scope.launch { + manga.value?.let { + updateChapter.await(chapterIds, read = read, onError = { toast(it.message.orEmpty()) }) + _selectedIds.value = persistentListOf() + loadChapters() + } + } + } + + fun markRead(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) + + fun markUnread(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) + + private fun setBookmarked( + chapterIds: List, + bookmark: Boolean, + ) { + scope.launch { + manga.value?.let { + updateChapter.await(chapterIds, bookmarked = bookmark, onError = { toast(it.message.orEmpty()) }) + _selectedIds.value = persistentListOf() + loadChapters() + } + } + } + + fun bookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) + + fun unBookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) + + fun markPreviousRead(index: Int) { + scope.launch { + manga.value?.let { + val chapters = chapters.value + .sortedBy { it.chapter.index } + .subList(0, index).map { it.chapter.id } // todo test + updateChapter.await(chapters, read = true, onError = { toast(it.message.orEmpty()) }) + _selectedIds.value = persistentListOf() + loadChapters() + } + } + } + + fun downloadChapter(chapterId: Long) { + scope.launch { queueChapterDownload.await(chapterId, onError = { toast(it.message.orEmpty()) }) } + } + + fun deleteDownload(id: Long?) { + scope.launch { + if (id == null) { + val chapterIds = _selectedIds.value + deleteChapterDownload.await(chapterIds, onError = { toast(it.message.orEmpty()) }) + selectedItems.value.forEach { + it.setNotDownloaded() + } + _selectedIds.value = persistentListOf() + } else { + chapters.value.find { it.chapter.id == id } + ?.deleteDownload(deleteChapterDownload) + } + } + } + + fun stopDownloadingChapter(chapterId: Long) { + scope.launch { + chapters.value.find { it.chapter.id == chapterId } + ?.stopDownloading(stopChapterDownload) + } + } + + fun selectAll() { + scope.launch { + _selectedIds.value = chapters.value.map { it.chapter.id }.toImmutableList() + } + } + + fun invertSelection() { + scope.launch { + _selectedIds.value = chapters.value.map { it.chapter.id }.minus(_selectedIds.value).toImmutableList() + } + } + + fun selectChapter(id: Long) { + scope.launch { + _selectedIds.value = _selectedIds.value.plus(id).toImmutableList() + } + } + + fun unselectChapter(id: Long) { + scope.launch { + _selectedIds.value = _selectedIds.value.minus(id).toImmutableList() + } + } + + fun clearSelection() { + scope.launch { + _selectedIds.value = persistentListOf() + } + } + + fun downloadChapters() { + scope.launch { + batchChapterDownload.await(_selectedIds.value) + _selectedIds.value = persistentListOf() + } + } + + fun downloadNext(next: Int) { + scope.launch { + batchChapterDownload.await( + _chapters.value.filter { !it.chapter.read && it.downloadState.value == ChapterDownloadState.NotDownloaded } + .map { it.chapter.id } + .takeLast(next), + ) + } + } + + fun downloadUnread() { + scope.launch { + batchChapterDownload.await( + _chapters.value.filter { !it.chapter.read && it.downloadState.value == ChapterDownloadState.NotDownloaded } + .map { it.chapter.id }, + ) + } + } + + fun downloadAll() { + scope.launch { + batchChapterDownload.await( + _chapters.value + .filter { it.downloadState.value == ChapterDownloadState.NotDownloaded } + .map { it.chapter.id }, + ) + } + } + + private fun List.toDownloadChapters() = + map { + ChapterDownloadItem(null, it) + }.toImmutableList() + + data class Params( + val mangaId: Long, + ) + + private companion object { + private val log = logging() + } +} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt index ecc9f129..9859fcfc 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt @@ -42,7 +42,6 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.MainScope -import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableSharedFlow @@ -69,372 +68,371 @@ import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class ReaderMenuViewModel - @Inject - constructor( - private val readerPreferences: ReaderPreferences, - private val getManga: GetManga, - private val getChapters: GetChapters, - private val getChapter: GetChapter, - private val getChapterPages: GetChapterPages, - private val updateChapter: UpdateChapter, - private val updateMangaMeta: UpdateMangaMeta, - private val updateChapterMeta: UpdateChapterMeta, - private val chapterCache: ChapterCache, - private val http: Http, - contextWrapper: ContextWrapper, - @Assisted private val params: Params, - ) : ViewModel(contextWrapper) { - override val scope = MainScope() - private val _manga = MutableStateFlow(null) - private val viewerChapters = MutableStateFlow(ViewerChapters(null, null, null)) - val previousChapter = viewerChapters.map { it.prevChapter }.stateIn(scope, SharingStarted.Eagerly, null) - val chapter = viewerChapters.map { it.currChapter }.stateIn(scope, SharingStarted.Eagerly, null) - val nextChapter = viewerChapters.map { it.nextChapter }.stateIn(scope, SharingStarted.Eagerly, null) +@Inject +class ReaderMenuViewModel( + private val readerPreferences: ReaderPreferences, + private val getManga: GetManga, + private val getChapters: GetChapters, + private val getChapter: GetChapter, + private val getChapterPages: GetChapterPages, + private val updateChapter: UpdateChapter, + private val updateMangaMeta: UpdateMangaMeta, + private val updateChapterMeta: UpdateChapterMeta, + private val chapterCache: ChapterCache, + private val http: Http, + contextWrapper: ContextWrapper, + @Assisted private val params: Params, +) : ViewModel(contextWrapper) { + override val scope = MainScope() + private val _manga = MutableStateFlow(null) + private val viewerChapters = MutableStateFlow(ViewerChapters(null, null, null)) + val previousChapter = viewerChapters.map { it.prevChapter }.stateIn(scope, SharingStarted.Eagerly, null) + val chapter = viewerChapters.map { it.currChapter }.stateIn(scope, SharingStarted.Eagerly, null) + val nextChapter = viewerChapters.map { it.nextChapter }.stateIn(scope, SharingStarted.Eagerly, null) - private val _state = MutableStateFlow(ReaderChapter.State.Wait) - val state = _state.asStateFlow() + private val _state = MutableStateFlow(ReaderChapter.State.Wait) + val state = _state.asStateFlow() - val pages = viewerChapters.flatMapLatest { viewerChapters -> - val previousChapterPages = viewerChapters.prevChapter - ?.pages - ?.map { (it as? PagesState.Success)?.pages } - ?: flowOf(null) - val chapterPages = viewerChapters.currChapter - ?.pages - ?.map { (it as? PagesState.Success)?.pages } - ?: flowOf(null) - val nextChapterPages = viewerChapters.nextChapter - ?.pages - ?.map { (it as? PagesState.Success)?.pages } - ?: flowOf(null) - combine(previousChapterPages, chapterPages, nextChapterPages) { prev, cur, next -> - ( - prev.orEmpty() + - ReaderPageSeparator(viewerChapters.prevChapter, viewerChapters.currChapter) + - cur.orEmpty() + - ReaderPageSeparator(viewerChapters.currChapter, viewerChapters.nextChapter) + - next.orEmpty() + val pages = viewerChapters.flatMapLatest { viewerChapters -> + val previousChapterPages = viewerChapters.prevChapter + ?.pages + ?.map { (it as? PagesState.Success)?.pages } + ?: flowOf(null) + val chapterPages = viewerChapters.currChapter + ?.pages + ?.map { (it as? PagesState.Success)?.pages } + ?: flowOf(null) + val nextChapterPages = viewerChapters.nextChapter + ?.pages + ?.map { (it as? PagesState.Success)?.pages } + ?: flowOf(null) + combine(previousChapterPages, chapterPages, nextChapterPages) { prev, cur, next -> + ( + prev.orEmpty() + + ReaderPageSeparator(viewerChapters.prevChapter, viewerChapters.currChapter) + + cur.orEmpty() + + ReaderPageSeparator(viewerChapters.currChapter, viewerChapters.nextChapter) + + next.orEmpty() ).toImmutableList() - } - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - - private val _currentPage = MutableStateFlow(null) - val currentPage = _currentPage.asStateFlow() - - private val _currentPageOffset = MutableStateFlow(1) - val currentPageOffset = _currentPageOffset.asStateFlow() - - private val _readerSettingsMenuOpen = MutableStateFlow(false) - val readerSettingsMenuOpen = _readerSettingsMenuOpen.asStateFlow() - - private val _pageEmitter = MutableSharedFlow() - val pageEmitter = StableHolder(_pageEmitter.asSharedFlow()) - - val readerModes = readerPreferences.modes() - .getAsFlow() - .map { it.toImmutableList() } - .stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - val readerMode = combine(readerPreferences.mode().getAsFlow(), _manga) { mode, manga -> - val mangaMode = manga?.meta?.juiReaderMode?.decodeURLQueryComponent() - if ( - mangaMode != null && - mangaMode != MangaMeta.DEFAULT_READER_MODE && - mangaMode in readerModes.value - ) { - mangaMode - } else { - mode - } - }.stateIn(scope, SharingStarted.Eagerly, readerPreferences.mode().get()) - - val readerModeSettings = ReaderModeWatch(readerPreferences, scope, readerMode) - - private val loader = ChapterLoader( - readerPreferences = readerPreferences, - http = http, - chapterCache = chapterCache, - bitmapDecoderFactory = BitmapDecoderFactory(contextWrapper), - getChapterPages = getChapterPages, - ) - - init { - init() } + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - fun init() { - scope.launchDefault { - runCatching { - initManga(params.mangaId) - initChapters(params.mangaId, params.chapterId) - } - } + private val _currentPage = MutableStateFlow(null) + val currentPage = _currentPage.asStateFlow() + + private val _currentPageOffset = MutableStateFlow(1) + val currentPageOffset = _currentPageOffset.asStateFlow() + + private val _readerSettingsMenuOpen = MutableStateFlow(false) + val readerSettingsMenuOpen = _readerSettingsMenuOpen.asStateFlow() + + private val _pageEmitter = MutableSharedFlow() + val pageEmitter = StableHolder(_pageEmitter.asSharedFlow()) + + val readerModes = readerPreferences.modes() + .getAsFlow() + .map { it.toImmutableList() } + .stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + val readerMode = combine(readerPreferences.mode().getAsFlow(), _manga) { mode, manga -> + val mangaMode = manga?.meta?.juiReaderMode?.decodeURLQueryComponent() + if ( + mangaMode != null && + mangaMode != MangaMeta.DEFAULT_READER_MODE && + mangaMode in readerModes.value + ) { + mangaMode + } else { + mode } + }.stateIn(scope, SharingStarted.Eagerly, readerPreferences.mode().get()) - init { - scope.launchDefault { - currentPage - .filterIsInstance() - .collectLatest { page -> - page.chapter.pageLoader?.loadPage(page) - if (page.chapter == chapter.value) { - val pages = page.chapter.pages.value as? PagesState.Success - ?: return@collectLatest - if ((page.index2 + 1) >= pages.pages.size) { - markChapterRead(page.chapter) - } - val nextChapter = nextChapter.value - if (nextChapter != null && (page.index2 + 1) >= ((pages.pages.size - 5).coerceAtLeast(1))) { - requestPreloadChapter(nextChapter) - } - } else { - val previousChapter = previousChapter.value - val nextChapter = nextChapter.value - if (page.chapter == previousChapter) { - viewerChapters.value = viewerChapters.value.movePrev() - initChapters(params.mangaId, page.chapter.chapter.id, fromMenuButton = false) - } else if (page.chapter == nextChapter) { - viewerChapters.value = viewerChapters.value.moveNext() - initChapters(params.mangaId, page.chapter.chapter.id, fromMenuButton = false) - } - } - } - } - } + val readerModeSettings = ReaderModeWatch(readerPreferences, scope, readerMode) - fun navigate(navigationRegion: Navigation): Boolean { - scope.launch { - val moveTo = when (navigationRegion) { - Navigation.MENU -> { - setReaderSettingsMenuOpen(!readerSettingsMenuOpen.value) - null - } + private val loader = ChapterLoader( + readerPreferences = readerPreferences, + http = http, + chapterCache = chapterCache, + bitmapDecoderFactory = BitmapDecoderFactory(contextWrapper), + getChapterPages = getChapterPages, + ) - Navigation.NEXT -> MoveTo.Next + init { + init() + } - Navigation.PREV -> MoveTo.Previous - - Navigation.RIGHT -> when (readerModeSettings.direction.value) { - Direction.Left -> MoveTo.Previous - else -> MoveTo.Next - } - - Navigation.LEFT -> when (readerModeSettings.direction.value) { - Direction.Left -> MoveTo.Next - else -> MoveTo.Previous - } - - Navigation.DOWN -> when (readerModeSettings.direction.value) { - Direction.Up -> MoveTo.Previous - else -> MoveTo.Next - } - - Navigation.UP -> when (readerModeSettings.direction.value) { - Direction.Up -> MoveTo.Next - else -> MoveTo.Previous - } - } - if (moveTo != null) { - _pageEmitter.emit(PageMove.Direction(moveTo)) - } - } - return true - } - - fun navigate(page: Int) { - log.info { "Navigate to $page" } - scope.launch { - _pageEmitter.emit(PageMove.Page(pages.value.getOrNull(page) ?: return@launch)) - } - } - - fun progress(page: ReaderItem) { - log.info { "Progressed to $page" } - _currentPage.value = page - } - - fun retry(page: ReaderPage) { - log.info { "Retrying ${page.index2}" } - chapter.value?.pageLoader?.retryPage(page) - } - - fun setMangaReaderMode(mode: String) { - scope.launchDefault { - _manga.value?.let { - updateMangaMeta.await(it, mode, onError = { toast(it.message.orEmpty()) }) - } + fun init() { + scope.launchDefault { + runCatching { initManga(params.mangaId) + initChapters(params.mangaId, params.chapterId) } } - - fun setReaderSettingsMenuOpen(open: Boolean) { - _readerSettingsMenuOpen.value = open - } - - fun prevChapter() { - scope.launchDefault { - val prevChapter = previousChapter.value ?: return@launchDefault - try { - _state.value = ReaderChapter.State.Wait - sendProgress() - viewerChapters.value = viewerChapters.value.movePrev() - initChapters(params.mangaId, prevChapter.chapter.id, fromMenuButton = true) - } catch (e: Exception) { - log.warn(e) { "Error loading prev chapter" } - } - } - } - - fun nextChapter() { - scope.launchDefault { - val nextChapter = nextChapter.value ?: return@launchDefault - try { - _state.value = ReaderChapter.State.Wait - sendProgress() - viewerChapters.value = viewerChapters.value.moveNext() - initChapters(params.mangaId, nextChapter.chapter.id, fromMenuButton = true) - } catch (e: Exception) { - log.warn(e) { "Error loading next chapter" } - } - } - } - - private suspend fun initManga(mangaId: Long) { - getManga.asFlow(mangaId) - .take(1) - .onEach { - _manga.value = it - } - .catch { - _state.value = ReaderChapter.State.Error(it) - log.warn(it) { "Error loading manga" } - } - .collect() - } - - private suspend fun initChapters( - mangaId: Long, - chapterId: Long, - fromMenuButton: Boolean = true, - ) { - log.debug { "Loading chapter index $chapterId" } - val (chapter, pages) = coroutineScope { - val chapters = getChapters.asFlow(mangaId) - .take(1) - .catch { - _state.value = ReaderChapter.State.Error(it) - log.warn(it) { "Error getting chapters for $mangaId" } - } - .singleOrNull() - ?: return@coroutineScope null - val chapter = chapters.find { it.id == chapterId } - ?.let { ReaderChapter(it) } - ?: return@coroutineScope null - val pages = loader.loadChapter(chapter) - viewerChapters.update { it.copy(currChapter = chapter) } - - if (viewerChapters.value.nextChapter == null) { - val nextChapter = chapters.find { it.index == chapter.chapter.index + 1 } - if (nextChapter != null) { - val nextReaderChapter = ReaderChapter(nextChapter) - viewerChapters.update { it.copy(nextChapter = nextReaderChapter) } - } else { - viewerChapters.update { it.copy(nextChapter = null) } - } - } - - if (viewerChapters.value.prevChapter == null) { - val prevChapter = chapters.find { it.index == chapter.chapter.index - 1 } - if (prevChapter != null) { - val prevReaderChapter = ReaderChapter(prevChapter) - viewerChapters.update { it.copy(prevChapter = prevReaderChapter) } - } else { - viewerChapters.update { it.copy(prevChapter = null) } - } - } - chapter to pages - } ?: return - - if (fromMenuButton) { - chapter.stateObserver - .onEach { - _state.value = it - } - .launchIn(chapter.scope) - - pages - .filterIsInstance() - .onEach { (pageList) -> - val lastPageReadOffset = chapter.chapter.meta.juiPageOffset - if (lastPageReadOffset != 0) { - _currentPageOffset.value = lastPageReadOffset - } - val lastPageRead = chapter.chapter.lastPageRead - _currentPage.value = if (lastPageRead > 0) { - pageList[lastPageRead.coerceAtMost(pageList.lastIndex)] - } else { - pageList.first() - }.also { chapter.pageLoader?.loadPage(it) } - } - .launchIn(chapter.scope) - } - } - - fun requestPreloadChapter(chapter: ReaderChapter) { - if (chapter.state != ReaderChapter.State.Wait && chapter.state !is ReaderChapter.State.Error) { - return - } - log.debug { "Preloading ${chapter.chapter.index}" } - loader.loadChapter(chapter) - } - - private fun markChapterRead(chapter: ReaderChapter) { - scope.launch { - updateChapter.await(chapter.chapter, read = true, onError = { toast(it.message.orEmpty()) }) - } - } - - @OptIn(DelicateCoroutinesApi::class) - fun sendProgress( - chapter: Chapter? = this.chapter.value?.chapter, - lastPageRead: Int = (currentPage.value as? ReaderPage)?.index2 ?: 0, - ) { - chapter ?: return - if (chapter.read) return - GlobalScope.launch { - updateChapter.await( - chapter, - lastPageRead = lastPageRead, - onError = { toast(it.message.orEmpty()) }, - ) - } - } - - fun updateLastPageReadOffset(offset: Int) { - updateLastPageReadOffset(chapter.value?.chapter ?: return, offset) - } - - @OptIn(DelicateCoroutinesApi::class) - private fun updateLastPageReadOffset( - chapter: Chapter, - offset: Int, - ) { - GlobalScope.launch { - updateChapterMeta.await(chapter, offset, onError = { toast(it.message.orEmpty()) }) - } - } - - override fun onDispose() { - viewerChapters.value.recycle() - scope.cancel() - } - - data class Params( - val chapterId: Long, - val mangaId: Long, - ) - - private companion object { - private val log = logging() - } } + + init { + scope.launchDefault { + currentPage + .filterIsInstance() + .collectLatest { page -> + page.chapter.pageLoader?.loadPage(page) + if (page.chapter == chapter.value) { + val pages = page.chapter.pages.value as? PagesState.Success + ?: return@collectLatest + if ((page.index2 + 1) >= pages.pages.size) { + markChapterRead(page.chapter) + } + val nextChapter = nextChapter.value + if (nextChapter != null && (page.index2 + 1) >= ((pages.pages.size - 5).coerceAtLeast(1))) { + requestPreloadChapter(nextChapter) + } + } else { + val previousChapter = previousChapter.value + val nextChapter = nextChapter.value + if (page.chapter == previousChapter) { + viewerChapters.value = viewerChapters.value.movePrev() + initChapters(params.mangaId, page.chapter.chapter.id, fromMenuButton = false) + } else if (page.chapter == nextChapter) { + viewerChapters.value = viewerChapters.value.moveNext() + initChapters(params.mangaId, page.chapter.chapter.id, fromMenuButton = false) + } + } + } + } + } + + fun navigate(navigationRegion: Navigation): Boolean { + scope.launch { + val moveTo = when (navigationRegion) { + Navigation.MENU -> { + setReaderSettingsMenuOpen(!readerSettingsMenuOpen.value) + null + } + + Navigation.NEXT -> MoveTo.Next + + Navigation.PREV -> MoveTo.Previous + + Navigation.RIGHT -> when (readerModeSettings.direction.value) { + Direction.Left -> MoveTo.Previous + else -> MoveTo.Next + } + + Navigation.LEFT -> when (readerModeSettings.direction.value) { + Direction.Left -> MoveTo.Next + else -> MoveTo.Previous + } + + Navigation.DOWN -> when (readerModeSettings.direction.value) { + Direction.Up -> MoveTo.Previous + else -> MoveTo.Next + } + + Navigation.UP -> when (readerModeSettings.direction.value) { + Direction.Up -> MoveTo.Next + else -> MoveTo.Previous + } + } + if (moveTo != null) { + _pageEmitter.emit(PageMove.Direction(moveTo)) + } + } + return true + } + + fun navigate(page: Int) { + log.info { "Navigate to $page" } + scope.launch { + _pageEmitter.emit(PageMove.Page(pages.value.getOrNull(page) ?: return@launch)) + } + } + + fun progress(page: ReaderItem) { + log.info { "Progressed to $page" } + _currentPage.value = page + } + + fun retry(page: ReaderPage) { + log.info { "Retrying ${page.index2}" } + chapter.value?.pageLoader?.retryPage(page) + } + + fun setMangaReaderMode(mode: String) { + scope.launchDefault { + _manga.value?.let { + updateMangaMeta.await(it, mode, onError = { toast(it.message.orEmpty()) }) + } + initManga(params.mangaId) + } + } + + fun setReaderSettingsMenuOpen(open: Boolean) { + _readerSettingsMenuOpen.value = open + } + + fun prevChapter() { + scope.launchDefault { + val prevChapter = previousChapter.value ?: return@launchDefault + try { + _state.value = ReaderChapter.State.Wait + sendProgress() + viewerChapters.value = viewerChapters.value.movePrev() + initChapters(params.mangaId, prevChapter.chapter.id, fromMenuButton = true) + } catch (e: Exception) { + log.warn(e) { "Error loading prev chapter" } + } + } + } + + fun nextChapter() { + scope.launchDefault { + val nextChapter = nextChapter.value ?: return@launchDefault + try { + _state.value = ReaderChapter.State.Wait + sendProgress() + viewerChapters.value = viewerChapters.value.moveNext() + initChapters(params.mangaId, nextChapter.chapter.id, fromMenuButton = true) + } catch (e: Exception) { + log.warn(e) { "Error loading next chapter" } + } + } + } + + private suspend fun initManga(mangaId: Long) { + getManga.asFlow(mangaId) + .take(1) + .onEach { + _manga.value = it + } + .catch { + _state.value = ReaderChapter.State.Error(it) + log.warn(it) { "Error loading manga" } + } + .collect() + } + + private suspend fun initChapters( + mangaId: Long, + chapterId: Long, + fromMenuButton: Boolean = true, + ) { + log.debug { "Loading chapter index $chapterId" } + val (chapter, pages) = coroutineScope { + val chapters = getChapters.asFlow(mangaId) + .take(1) + .catch { + _state.value = ReaderChapter.State.Error(it) + log.warn(it) { "Error getting chapters for $mangaId" } + } + .singleOrNull() + ?: return@coroutineScope null + val chapter = chapters.find { it.id == chapterId } + ?.let { ReaderChapter(it) } + ?: return@coroutineScope null + val pages = loader.loadChapter(chapter) + viewerChapters.update { it.copy(currChapter = chapter) } + + if (viewerChapters.value.nextChapter == null) { + val nextChapter = chapters.find { it.index == chapter.chapter.index + 1 } + if (nextChapter != null) { + val nextReaderChapter = ReaderChapter(nextChapter) + viewerChapters.update { it.copy(nextChapter = nextReaderChapter) } + } else { + viewerChapters.update { it.copy(nextChapter = null) } + } + } + + if (viewerChapters.value.prevChapter == null) { + val prevChapter = chapters.find { it.index == chapter.chapter.index - 1 } + if (prevChapter != null) { + val prevReaderChapter = ReaderChapter(prevChapter) + viewerChapters.update { it.copy(prevChapter = prevReaderChapter) } + } else { + viewerChapters.update { it.copy(prevChapter = null) } + } + } + chapter to pages + } ?: return + + if (fromMenuButton) { + chapter.stateObserver + .onEach { + _state.value = it + } + .launchIn(chapter.scope) + + pages + .filterIsInstance() + .onEach { (pageList) -> + val lastPageReadOffset = chapter.chapter.meta.juiPageOffset + if (lastPageReadOffset != 0) { + _currentPageOffset.value = lastPageReadOffset + } + val lastPageRead = chapter.chapter.lastPageRead + _currentPage.value = if (lastPageRead > 0) { + pageList[lastPageRead.coerceAtMost(pageList.lastIndex)] + } else { + pageList.first() + }.also { chapter.pageLoader?.loadPage(it) } + } + .launchIn(chapter.scope) + } + } + + fun requestPreloadChapter(chapter: ReaderChapter) { + if (chapter.state != ReaderChapter.State.Wait && chapter.state !is ReaderChapter.State.Error) { + return + } + log.debug { "Preloading ${chapter.chapter.index}" } + loader.loadChapter(chapter) + } + + private fun markChapterRead(chapter: ReaderChapter) { + scope.launch { + updateChapter.await(chapter.chapter, read = true, onError = { toast(it.message.orEmpty()) }) + } + } + + @OptIn(DelicateCoroutinesApi::class) + fun sendProgress( + chapter: Chapter? = this.chapter.value?.chapter, + lastPageRead: Int = (currentPage.value as? ReaderPage)?.index2 ?: 0, + ) { + chapter ?: return + if (chapter.read) return + GlobalScope.launch { + updateChapter.await( + chapter, + lastPageRead = lastPageRead, + onError = { toast(it.message.orEmpty()) }, + ) + } + } + + fun updateLastPageReadOffset(offset: Int) { + updateLastPageReadOffset(chapter.value?.chapter ?: return, offset) + } + + @OptIn(DelicateCoroutinesApi::class) + private fun updateLastPageReadOffset( + chapter: Chapter, + offset: Int, + ) { + GlobalScope.launch { + updateChapterMeta.await(chapter, offset, onError = { toast(it.message.orEmpty()) }) + } + } + + override fun onDispose() { + viewerChapters.value.recycle() + scope.cancel() + } + + data class Params( + val chapterId: Long, + val mangaId: Long, + ) + + private companion object { + private val log = logging() + } +} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAdvancedScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAdvancedScreen.kt index 82b5baf6..b723ae43 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAdvancedScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAdvancedScreen.kt @@ -73,47 +73,46 @@ class SettingsAdvancedScreen : Screen { } } -class SettingsAdvancedViewModel - @Inject - constructor( - updatePreferences: UpdatePreferences, - private val imageCache: ImageCache, - private val chapterCache: ChapterCache, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - val updatesEnabled = updatePreferences.enabled().asStateFlow() +@Inject +class SettingsAdvancedViewModel( + updatePreferences: UpdatePreferences, + private val imageCache: ImageCache, + private val chapterCache: ChapterCache, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + val updatesEnabled = updatePreferences.enabled().asStateFlow() - val imageCacheSize = flow { - while (currentCoroutineContext().isActive) { - emit(imageCache.size.bytesIntoHumanReadable()) - delay(1.seconds) - } - }.stateIn(scope, SharingStarted.Eagerly, "") - - val chapterCacheSize = flow { - while (currentCoroutineContext().isActive) { - emit(chapterCache.size.bytesIntoHumanReadable()) - delay(1.seconds) - } - }.stateIn(scope, SharingStarted.Eagerly, "") - - fun clearImageCache() { - scope.launchIO { - imageCache.clear() - } + val imageCacheSize = flow { + while (currentCoroutineContext().isActive) { + emit(imageCache.size.bytesIntoHumanReadable()) + delay(1.seconds) } + }.stateIn(scope, SharingStarted.Eagerly, "") - fun clearChapterCache() { - scope.launchIO { - chapterCache.clear() - } + val chapterCacheSize = flow { + while (currentCoroutineContext().isActive) { + emit(chapterCache.size.bytesIntoHumanReadable()) + delay(1.seconds) } + }.stateIn(scope, SharingStarted.Eagerly, "") - companion object { - private val log = logging() + fun clearImageCache() { + scope.launchIO { + imageCache.clear() } } + fun clearChapterCache() { + scope.launchIO { + chapterCache.clear() + } + } + + companion object { + private val log = logging() + } +} + @Composable fun SettingsAdvancedScreenContent( updatesEnabled: PreferenceMutableStateFlow, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAppearanceScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAppearanceScreen.kt index 9622ee72..3edd4bd3 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAppearanceScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAppearanceScreen.kt @@ -108,23 +108,22 @@ class SettingsAppearanceScreen : Screen { } } -class ThemesViewModel - @Inject - constructor( - private val uiPreferences: UiPreferences, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - val themeMode = uiPreferences.themeMode().asStateFlow() - val lightTheme = uiPreferences.lightTheme().asStateFlow() - val darkTheme = uiPreferences.darkTheme().asStateFlow() - val lightColors = uiPreferences.getLightColors().asStateFlow(scope) - val darkColors = uiPreferences.getDarkColors().asStateFlow(scope) +@Inject +class ThemesViewModel( + private val uiPreferences: UiPreferences, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + val themeMode = uiPreferences.themeMode().asStateFlow() + val lightTheme = uiPreferences.lightTheme().asStateFlow() + val darkTheme = uiPreferences.darkTheme().asStateFlow() + val lightColors = uiPreferences.getLightColors().asStateFlow(scope) + val darkColors = uiPreferences.getDarkColors().asStateFlow(scope) - val windowDecorations = uiPreferences.windowDecorations().asStateFlow() + val windowDecorations = uiPreferences.windowDecorations().asStateFlow() - @Composable - fun getActiveColors(): AppColorsPreferenceState = if (MaterialTheme.colors.isLight) lightColors else darkColors - } + @Composable + fun getActiveColors(): AppColorsPreferenceState = if (MaterialTheme.colors.isLight) lightColors else darkColors +} expect val showWindowDecorationsOption: Boolean diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsBackupScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsBackupScreen.kt index 32ab49c6..5a6db719 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsBackupScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsBackupScreen.kt @@ -118,157 +118,156 @@ class SettingsBackupScreen : Screen { } } -class SettingsBackupViewModel - @Inject - constructor( - private val validateBackupFile: ValidateBackupFile, - private val importBackupFile: ImportBackupFile, - private val exportBackupFile: ExportBackupFile, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - private val _restoreStatus = MutableStateFlow(Status.Nothing) - val restoreStatus = _restoreStatus.asStateFlow() +@Inject +class SettingsBackupViewModel( + private val validateBackupFile: ValidateBackupFile, + private val importBackupFile: ImportBackupFile, + private val exportBackupFile: ExportBackupFile, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + private val _restoreStatus = MutableStateFlow(Status.Nothing) + val restoreStatus = _restoreStatus.asStateFlow() - private val _missingSourceFlow = MutableSharedFlow>>() - val missingSourceFlowHolder = StableHolder(_missingSourceFlow.asSharedFlow()) + private val _missingSourceFlow = MutableSharedFlow>>() + val missingSourceFlowHolder = StableHolder(_missingSourceFlow.asSharedFlow()) - private val _creatingStatus = MutableStateFlow(Status.Nothing) - val creatingStatus = _creatingStatus.asStateFlow() - private val _createFlow = MutableSharedFlow() - val createFlowHolder = StableHolder(_createFlow.asSharedFlow()) + private val _creatingStatus = MutableStateFlow(Status.Nothing) + val creatingStatus = _creatingStatus.asStateFlow() + private val _createFlow = MutableSharedFlow() + val createFlowHolder = StableHolder(_createFlow.asSharedFlow()) - fun restoreFile(source: Source) { - scope.launch { - val file = try { - FileSystem.SYSTEM_TEMPORARY_DIRECTORY - .resolve("tachidesk.${Random.nextLong()}.tachibk") - .also { file -> - source.saveTo(file) - } - } catch (e: Exception) { - log.warn(e) { "Error creating backup file" } - _restoreStatus.value = Status.Error - e.throwIfCancellation() - return@launch - } - - validateBackupFile.asFlow(file) - .onEach { (missingSources) -> - if (missingSources.isEmpty()) { - restoreBackup(file) - } else { - _missingSourceFlow.emit(file to missingSources.toImmutableList()) - } + fun restoreFile(source: Source) { + scope.launch { + val file = try { + FileSystem.SYSTEM_TEMPORARY_DIRECTORY + .resolve("tachidesk.${Random.nextLong()}.tachibk") + .also { file -> + source.saveTo(file) } - .catch { - toast(it.message.orEmpty()) - log.warn(it) { "Error importing backup" } - _restoreStatus.value = Status.Error - } - .collect() + } catch (e: Exception) { + log.warn(e) { "Error creating backup file" } + _restoreStatus.value = Status.Error + e.throwIfCancellation() + return@launch } - } - fun restoreBackup(file: Path) { - importBackupFile - .asFlow(file) - .onStart { - _restoreStatus.value = Status.InProgress(null) - } - .onEach { - _restoreStatus.value = it.second.toStatus() + validateBackupFile.asFlow(file) + .onEach { (missingSources) -> + if (missingSources.isEmpty()) { + restoreBackup(file) + } else { + _missingSourceFlow.emit(file to missingSources.toImmutableList()) + } } .catch { toast(it.message.orEmpty()) log.warn(it) { "Error importing backup" } _restoreStatus.value = Status.Error } - .launchIn(scope) + .collect() } + } - private fun RestoreStatus.toStatus() = when (state) { - RestoreState.IDLE -> Status.Success - RestoreState.SUCCESS -> Status.Success - RestoreState.FAILURE -> Status.Error - RestoreState.RESTORING_CATEGORIES -> Status.InProgress(0.01f) - RestoreState.RESTORING_SETTINGS -> Status.InProgress(0.02f) - RestoreState.RESTORING_MANGA -> Status.InProgress((completed.toFloat() / total).coerceIn(0f, 0.99f)) - RestoreState.RESTORING_META -> Status.InProgress(1f) - RestoreState.UNKNOWN -> Status.Error - } + fun restoreBackup(file: Path) { + importBackupFile + .asFlow(file) + .onStart { + _restoreStatus.value = Status.InProgress(null) + } + .onEach { + _restoreStatus.value = it.second.toStatus() + } + .catch { + toast(it.message.orEmpty()) + log.warn(it) { "Error importing backup" } + _restoreStatus.value = Status.Error + } + .launchIn(scope) + } - fun stopRestore() { - _restoreStatus.value = Status.Error - } + private fun RestoreStatus.toStatus() = when (state) { + RestoreState.IDLE -> Status.Success + RestoreState.SUCCESS -> Status.Success + RestoreState.FAILURE -> Status.Error + RestoreState.RESTORING_CATEGORIES -> Status.InProgress(0.01f) + RestoreState.RESTORING_SETTINGS -> Status.InProgress(0.02f) + RestoreState.RESTORING_MANGA -> Status.InProgress((completed.toFloat() / total).coerceIn(0f, 0.99f)) + RestoreState.RESTORING_META -> Status.InProgress(1f) + RestoreState.UNKNOWN -> Status.Error + } - private val tempFile = MutableStateFlow(null) - private val mutex = Mutex() + fun stopRestore() { + _restoreStatus.value = Status.Error + } - fun exportBackup() { - exportBackupFile - .asFlow( - true, true // todo - ) { - onDownload { bytesSentTotal, contentLength -> - _creatingStatus.value = Status.InProgress( - (bytesSentTotal.toFloat() / contentLength) - .coerceAtMost(0.99F), - ) - } + private val tempFile = MutableStateFlow(null) + private val mutex = Mutex() + + fun exportBackup() { + exportBackupFile + .asFlow( + true, true, // todo + ) { + onDownload { bytesSentTotal, contentLength -> + _creatingStatus.value = Status.InProgress( + (bytesSentTotal.toFloat() / contentLength) + .coerceAtMost(0.99F), + ) } - .onStart { - _creatingStatus.value = Status.InProgress(null) - } - .onEach { (filename, source) -> - tempFile.value = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(filename).also { - mutex.tryLock() - scope.launch { - try { - source.saveTo(it) - } catch (e: Exception) { - e.throwIfCancellation() - log.warn(e) { "Error creating backup" } - _creatingStatus.value = Status.Error - } finally { - mutex.unlock() - } - } - } - _createFlow.emit(filename) - } - .catch { - toast(it.message.orEmpty()) - log.warn(it) { "Error exporting backup" } - _creatingStatus.value = Status.Error - } - .launchIn(scope) - } - - fun exportBackupFileFound(backupSink: Sink) { - scope.launch { - mutex.withLock { - val tempFile = tempFile.value - if (creatingStatus.value is Status.InProgress && tempFile != null) { + } + .onStart { + _creatingStatus.value = Status.InProgress(null) + } + .onEach { (filename, source) -> + tempFile.value = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(filename).also { + mutex.tryLock() + scope.launch { try { - FileSystem.SYSTEM.source(tempFile).copyTo(backupSink.buffer()) - _creatingStatus.value = Status.Success + source.saveTo(it) } catch (e: Exception) { e.throwIfCancellation() - log.error(e) { "Error moving created backup" } + log.warn(e) { "Error creating backup" } _creatingStatus.value = Status.Error + } finally { + mutex.unlock() } - } else { + } + } + _createFlow.emit(filename) + } + .catch { + toast(it.message.orEmpty()) + log.warn(it) { "Error exporting backup" } + _creatingStatus.value = Status.Error + } + .launchIn(scope) + } + + fun exportBackupFileFound(backupSink: Sink) { + scope.launch { + mutex.withLock { + val tempFile = tempFile.value + if (creatingStatus.value is Status.InProgress && tempFile != null) { + try { + FileSystem.SYSTEM.source(tempFile).copyTo(backupSink.buffer()) + _creatingStatus.value = Status.Success + } catch (e: Exception) { + e.throwIfCancellation() + log.error(e) { "Error moving created backup" } _creatingStatus.value = Status.Error } + } else { + _creatingStatus.value = Status.Error } } } - - private companion object { - private val log = logging() - } } + private companion object { + private val log = logging() + } +} + sealed class Status { data object Nothing : Status() diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsGeneralScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsGeneralScreen.kt index dbda2421..a974c4fd 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsGeneralScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsGeneralScreen.kt @@ -78,63 +78,67 @@ class SettingsGeneralScreen : Screen { } } -class SettingsGeneralViewModel - @Inject - constructor( - private val dateHandler: DateHandler, - uiPreferences: UiPreferences, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - val startScreen = uiPreferences.startScreen().asStateFlow() - val confirmExit = uiPreferences.confirmExit().asStateFlow() - val language = uiPreferences.language().asStateFlow() - val dateFormat = uiPreferences.dateFormat().asStateFlow() +@Inject +class SettingsGeneralViewModel( + private val dateHandler: DateHandler, + uiPreferences: UiPreferences, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + val startScreen = uiPreferences.startScreen().asStateFlow() + val confirmExit = uiPreferences.confirmExit().asStateFlow() + val language = uiPreferences.language().asStateFlow() + val dateFormat = uiPreferences.dateFormat().asStateFlow() - private val now = Clock.System.now() - private val currentLocale = Locale.current + private val now = Clock.System.now() + private val currentLocale = Locale.current - @Composable - fun getStartScreenChoices(): ImmutableMap = - persistentMapOf( - StartScreen.Library to stringResource(MR.strings.location_library), - StartScreen.Updates to stringResource(MR.strings.location_updates), - StartScreen.Sources to stringResource(MR.strings.location_sources), - StartScreen.Extensions to stringResource(MR.strings.location_extensions), - ) + @Composable + fun getStartScreenChoices(): ImmutableMap = + persistentMapOf( + StartScreen.Library to stringResource(MR.strings.location_library), + StartScreen.Updates to stringResource(MR.strings.location_updates), + StartScreen.Sources to stringResource(MR.strings.location_sources), + StartScreen.Extensions to stringResource(MR.strings.location_extensions), + ) - @Composable - fun getLanguageChoices(): ImmutableMap { - val langJsonState = MR.files.languages_json.readTextAsync() - val langs by produceState(emptyMap(), langJsonState.value) { - val langJson = langJsonState.value - if (langJson != null) { - withIOContext { - value = Json.decodeFromString(langJson)["langs"] - ?.jsonArray - .orEmpty() - .map { it.jsonPrimitive.content } - .associateWith { Locale(it).getDisplayName(currentLocale) } - } + @Composable + fun getLanguageChoices(): ImmutableMap { + val langJsonState = MR.files.languages_json.readTextAsync() + val langs by produceState(emptyMap(), langJsonState.value) { + val langJson = langJsonState.value + if (langJson != null) { + withIOContext { + value = Json.decodeFromString(langJson)["langs"] + ?.jsonArray + .orEmpty() + .map { it.jsonPrimitive.content } + .associateWith { Locale(it).getDisplayName(currentLocale) } } } - return mapOf("" to stringResource(MR.strings.language_system_default, currentLocale.getDisplayName(currentLocale))) - .plus(langs) - .toImmutableMap() } - - @Composable - fun getDateChoices(): ImmutableMap = - dateHandler.formatOptions - .associateWith { - it.ifEmpty { stringResource(MR.strings.date_system_default) } + - " (${getFormattedDate(it)})" - } - .toImmutableMap() - - @Composable - private fun getFormattedDate(prefValue: String): String = dateHandler.getDateFormat(prefValue).invoke(now) + return mapOf( + "" to stringResource( + MR.strings.language_system_default, + currentLocale.getDisplayName(currentLocale), + ), + ) + .plus(langs) + .toImmutableMap() } + @Composable + fun getDateChoices(): ImmutableMap = + dateHandler.formatOptions + .associateWith { + it.ifEmpty { stringResource(MR.strings.date_system_default) } + + " (${getFormattedDate(it)})" + } + .toImmutableMap() + + @Composable + private fun getFormattedDate(prefValue: String): String = dateHandler.getDateFormat(prefValue).invoke(now) +} + @Composable fun SettingsGeneralScreenContent( startScreen: PreferenceMutableStateFlow, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsLibraryScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsLibraryScreen.kt index 2932091c..7da6def1 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsLibraryScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsLibraryScreen.kt @@ -94,38 +94,37 @@ class SettingsLibraryScreen : Screen { } } -class SettingsLibraryViewModel - @Inject - constructor( - libraryPreferences: LibraryPreferences, - private val getCategories: GetCategories, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - val displayMode = libraryPreferences.displayMode().asStateFlow() - val gridColumns = libraryPreferences.gridColumns().asStateFlow() - val gridSize = libraryPreferences.gridSize().asStateFlow() +@Inject +class SettingsLibraryViewModel( + libraryPreferences: LibraryPreferences, + private val getCategories: GetCategories, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + val displayMode = libraryPreferences.displayMode().asStateFlow() + val gridColumns = libraryPreferences.gridColumns().asStateFlow() + val gridSize = libraryPreferences.gridSize().asStateFlow() - val showAllCategory = libraryPreferences.showAllCategory().asStateFlow() - private val _categories = MutableStateFlow(0) - val categories = _categories.asStateFlow() + val showAllCategory = libraryPreferences.showAllCategory().asStateFlow() + private val _categories = MutableStateFlow(0) + val categories = _categories.asStateFlow() - init { - refreshCategoryCount() - } - - fun refreshCategoryCount() { - scope.launch { - _categories.value = getCategories.await(true, onError = { toast(it.message.orEmpty()) })?.size ?: 0 - } - } - - @Composable - fun getDisplayModeChoices() = - DisplayMode.entries - .associateWith { stringResource(it.res) } - .toImmutableMap() + init { + refreshCategoryCount() } + fun refreshCategoryCount() { + scope.launch { + _categories.value = getCategories.await(true, onError = { toast(it.message.orEmpty()) })?.size ?: 0 + } + } + + @Composable + fun getDisplayModeChoices() = + DisplayMode.entries + .associateWith { stringResource(it.res) } + .toImmutableMap() +} + @Composable fun SettingsLibraryScreenContent( displayMode: PreferenceMutableStateFlow, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsReaderScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsReaderScreen.kt index 5a928da2..ba5f9225 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsReaderScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsReaderScreen.kt @@ -88,75 +88,75 @@ class SettingsReaderScreen : Screen { } } -class SettingsReaderViewModel - @Inject - constructor( - readerPreferences: ReaderPreferences, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - val modes = readerPreferences.modes().asStateFlow() - .map { - it.associateWith { it } - .toImmutableMap() - } - .stateIn(scope, SharingStarted.Eagerly, persistentMapOf()) - val selectedMode = readerPreferences.mode().asStateIn(scope) +@Inject +class SettingsReaderViewModel( + readerPreferences: ReaderPreferences, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + val modes = readerPreferences.modes().asStateFlow() + .map { + it.associateWith { it } + .toImmutableMap() + } + .stateIn(scope, SharingStarted.Eagerly, persistentMapOf()) + val selectedMode = readerPreferences.mode().asStateIn(scope) - private val _modeSettings = MutableStateFlow>>( - persistentListOf(), - ) - val modeSettings = _modeSettings.asStateFlow() + private val _modeSettings = MutableStateFlow>>( + persistentListOf(), + ) + val modeSettings = _modeSettings.asStateFlow() - init { - modes.onEach { modes -> - val modeSettings = _modeSettings.value - val modesInSettings = modeSettings.map { it.item.mode } - _modeSettings.value = modeSettings.filter { it.item.mode in modes }.toPersistentList() + modes.filter { (it) -> + init { + modes.onEach { modes -> + val modeSettings = _modeSettings.value + val modesInSettings = modeSettings.map { it.item.mode } + _modeSettings.value = + modeSettings.filter { it.item.mode in modes }.toPersistentList() + modes.filter { (it) -> it !in modesInSettings }.map { (it) -> StableHolder(ReaderModePreference(scope, it, readerPreferences.getMode(it))) } - }.launchIn(scope) - } - - fun getDirectionChoices() = - Direction.entries.associateWith { it.res.toPlatformString() } - .toImmutableMap() - - fun getPaddingChoices() = - mapOf( - 0 to MR.strings.page_padding_none.toPlatformString(), - 8 to "8 Dp", - 16 to "16 Dp", - 32 to "32 Dp", - ).toImmutableMap() - - fun getMaxSizeChoices(direction: Direction) = - if (direction == Direction.Right || direction == Direction.Left) { - mapOf( - 0 to MR.strings.max_size_unrestricted.toPlatformString(), - 700 to "700 Dp", - 900 to "900 Dp", - 1100 to "1100 Dp", - ) - } else { - mapOf( - 0 to MR.strings.max_size_unrestricted.toPlatformString(), - 500 to "500 Dp", - 700 to "700 Dp", - 900 to "900 Dp", - ) - }.toImmutableMap() - - fun getImageScaleChoices() = - ImageScale.entries.associateWith { it.res.toPlatformString() } - .toImmutableMap() - - fun getNavigationModeChoices() = - NavigationMode.entries.associateWith { it.res.toPlatformString() } - .toImmutableMap() + }.launchIn(scope) } + fun getDirectionChoices() = + Direction.entries.associateWith { it.res.toPlatformString() } + .toImmutableMap() + + fun getPaddingChoices() = + mapOf( + 0 to MR.strings.page_padding_none.toPlatformString(), + 8 to "8 Dp", + 16 to "16 Dp", + 32 to "32 Dp", + ).toImmutableMap() + + fun getMaxSizeChoices(direction: Direction) = + if (direction == Direction.Right || direction == Direction.Left) { + mapOf( + 0 to MR.strings.max_size_unrestricted.toPlatformString(), + 700 to "700 Dp", + 900 to "900 Dp", + 1100 to "1100 Dp", + ) + } else { + mapOf( + 0 to MR.strings.max_size_unrestricted.toPlatformString(), + 500 to "500 Dp", + 700 to "700 Dp", + 900 to "900 Dp", + ) + }.toImmutableMap() + + fun getImageScaleChoices() = + ImageScale.entries.associateWith { it.res.toPlatformString() } + .toImmutableMap() + + fun getNavigationModeChoices() = + NavigationMode.entries.associateWith { it.res.toPlatformString() } + .toImmutableMap() +} + data class ReaderModePreference( val scope: CoroutineScope, val mode: String, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt index 95666216..ee2d5f78 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt @@ -191,7 +191,8 @@ class ServerSettings( getSetting = { it.backupTime }, getInput = { SetSettingsInput(backupTime = it) }, ) -// val basicAuthEnabled = getServerFlow( + + // val basicAuthEnabled = getServerFlow( // getSetting = { it.basicAuthEnabled }, // getInput = { SetSettingsInput(basicAuthEnabled = it) }, // ) @@ -263,7 +264,8 @@ class ServerSettings( getSetting = { it.globalUpdateInterval.toString() }, getInput = { SetSettingsInput(globalUpdateInterval = it.toDoubleOrNull()?.takeIf { it !in 0.01..5.99 }) }, ) -// val gqlDebugLogsEnabled = getServerFlow( + + // val gqlDebugLogsEnabled = getServerFlow( // getSetting = { it.gqlDebugLogsEnabled }, // getInput = { SetSettingsInput(gqlDebugLogsEnabled = it) }, // ) @@ -360,67 +362,66 @@ class ServerSettings( ) } -class SettingsServerViewModel - @Inject - constructor( - private val getSettings: GetSettings, - private val setSettings: SetSettings, - serverPreferences: ServerPreferences, - serverHostPreferences: ServerHostPreferences, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - val serverUrl = serverPreferences.server().asStateIn(scope) - val serverPort = serverPreferences.port().asStringStateIn(scope) - val serverPathPrefix = serverPreferences.pathPrefix().asStateIn(scope) +@Inject +class SettingsServerViewModel( + private val getSettings: GetSettings, + private val setSettings: SetSettings, + serverPreferences: ServerPreferences, + serverHostPreferences: ServerHostPreferences, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + val serverUrl = serverPreferences.server().asStateIn(scope) + val serverPort = serverPreferences.port().asStringStateIn(scope) + val serverPathPrefix = serverPreferences.pathPrefix().asStateIn(scope) - val proxy = serverPreferences.proxy().asStateIn(scope) + val proxy = serverPreferences.proxy().asStateIn(scope) - val host = serverHostPreferences.host().asStateIn(scope) + val host = serverHostPreferences.host().asStateIn(scope) - @Composable - fun getProxyChoices(): ImmutableMap = - persistentMapOf( - Proxy.NO_PROXY to stringResource(MR.strings.no_proxy), - Proxy.HTTP_PROXY to stringResource(MR.strings.http_proxy), - Proxy.SOCKS_PROXY to stringResource(MR.strings.socks_proxy), - ) + @Composable + fun getProxyChoices(): ImmutableMap = + persistentMapOf( + Proxy.NO_PROXY to stringResource(MR.strings.no_proxy), + Proxy.HTTP_PROXY to stringResource(MR.strings.http_proxy), + Proxy.SOCKS_PROXY to stringResource(MR.strings.socks_proxy), + ) - val httpHost = serverPreferences.proxyHttpHost().asStateIn(scope) - val httpPort = serverPreferences.proxyHttpPort().asStringStateIn(scope) - val socksHost = serverPreferences.proxySocksHost().asStateIn(scope) - val socksPort = serverPreferences.proxySocksPort().asStringStateIn(scope) + val httpHost = serverPreferences.proxyHttpHost().asStateIn(scope) + val httpPort = serverPreferences.proxyHttpPort().asStringStateIn(scope) + val socksHost = serverPreferences.proxySocksHost().asStateIn(scope) + val socksPort = serverPreferences.proxySocksPort().asStringStateIn(scope) - val auth = serverPreferences.auth().asStateIn(scope) + val auth = serverPreferences.auth().asStateIn(scope) - @Composable - fun getAuthChoices(): ImmutableMap = - persistentMapOf( - Auth.NONE to stringResource(MR.strings.no_auth), - Auth.BASIC to stringResource(MR.strings.basic_auth), - Auth.DIGEST to stringResource(MR.strings.digest_auth), - ) + @Composable + fun getAuthChoices(): ImmutableMap = + persistentMapOf( + Auth.NONE to stringResource(MR.strings.no_auth), + Auth.BASIC to stringResource(MR.strings.basic_auth), + Auth.DIGEST to stringResource(MR.strings.digest_auth), + ) - val authUsername = serverPreferences.authUsername().asStateIn(scope) - val authPassword = serverPreferences.authPassword().asStateIn(scope) + val authUsername = serverPreferences.authUsername().asStateIn(scope) + val authPassword = serverPreferences.authPassword().asStateIn(scope) - private val _serverSettings = MutableStateFlow(null) - val serverSettings = _serverSettings.asStateFlow() + private val _serverSettings = MutableStateFlow(null) + val serverSettings = _serverSettings.asStateFlow() - init { - scope.launchIO { - val initialSettings = getSettings.await(onError = { toast(it.message.orEmpty()) }) - if (initialSettings != null) { - _serverSettings.value = ServerSettings( - getSettings, - setSettings, - scope, - initialSettings, - onError = { toast(it) }, - ) - } + init { + scope.launchIO { + val initialSettings = getSettings.await(onError = { toast(it.message.orEmpty()) }) + if (initialSettings != null) { + _serverSettings.value = ServerSettings( + getSettings, + setSettings, + scope, + initialSettings, + onError = { toast(it) }, + ) } } } +} @Composable fun SettingsServerScreenContent( @@ -886,7 +887,7 @@ private val repoRegex = ( "https:\\/\\/(?>www\\.|raw\\.)?(github|githubusercontent)\\.com" + "\\/([^\\/]+)\\/([^\\/]+)(?>(?>\\/tree|\\/blob)?\\/([^\\/\\n]*))?(?>\\/([^\\/\\n]*\\.json)?)?" - ).toRegex() + ).toRegex() @Composable fun ExtensionReposDialog( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/GlobalSearchViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/GlobalSearchViewModel.kt index 59f1d34a..ac5823bb 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/GlobalSearchViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/GlobalSearchViewModel.kt @@ -43,129 +43,128 @@ import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GlobalSearchViewModel - @Inject - constructor( - private val getSourceList: GetSourceList, - private val getSearchManga: GetSearchManga, - catalogPreferences: CatalogPreferences, - contextWrapper: ContextWrapper, - @Assisted private val savedStateHandle: SavedStateHandle, - @Assisted params: Params, - ) : ViewModel(contextWrapper) { - private val _query by savedStateHandle.getStateFlow { params.initialQuery } - val query = _query.asStateFlow() +@Inject +class GlobalSearchViewModel( + private val getSourceList: GetSourceList, + private val getSearchManga: GetSearchManga, + catalogPreferences: CatalogPreferences, + contextWrapper: ContextWrapper, + @Assisted private val savedStateHandle: SavedStateHandle, + @Assisted params: Params, +) : ViewModel(contextWrapper) { + private val _query by savedStateHandle.getStateFlow { params.initialQuery } + val query = _query.asStateFlow() - private val installedSources = MutableStateFlow(emptyList()) + private val installedSources = MutableStateFlow(emptyList()) - private val languages = catalogPreferences.languages().stateIn(scope) - val displayMode = catalogPreferences.displayMode().stateIn(scope) + private val languages = catalogPreferences.languages().stateIn(scope) + val displayMode = catalogPreferences.displayMode().stateIn(scope) - private val _isLoading = MutableStateFlow(true) - val isLoading = _isLoading.asStateFlow() + private val _isLoading = MutableStateFlow(true) + val isLoading = _isLoading.asStateFlow() - val sources = combine(installedSources, languages) { installedSources, languages -> - installedSources.filter { - it.lang in languages || it.id == Source.LOCAL_SOURCE_ID - }.toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + val sources = combine(installedSources, languages) { installedSources, languages -> + installedSources.filter { + it.lang in languages || it.id == Source.LOCAL_SOURCE_ID + }.toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - private val search by savedStateHandle.getStateFlow { params.initialQuery } + private val search by savedStateHandle.getStateFlow { params.initialQuery } - val results = SnapshotStateMap() + val results = SnapshotStateMap() - init { - getSources() - readySearch() - } + init { + getSources() + readySearch() + } - private fun getSources() { - getSourceList.asFlow() - .onEach { sources -> - installedSources.value = sources.sortedWith( - compareBy(String.CASE_INSENSITIVE_ORDER) { it.lang } - .thenBy(String.CASE_INSENSITIVE_ORDER) { - it.name - }, - ) - _isLoading.value = false - } - .catch { - toast(it.message.orEmpty()) - log.warn(it) { "Error getting sources" } - _isLoading.value = false - } - .launchIn(scope) - } - - private val semaphore = Semaphore(5) - - private fun readySearch() { - search - .combine(sources) { query, sources -> - query to sources - } - .mapLatest { (query, sources) -> - results.clear() - supervisorScope { - sources.map { source -> - async { - semaphore.withPermit { - getSearchManga.asFlow(source, 1, query, null) - .map { - if (it.mangaList.isEmpty()) { - Search.Failure(MR.strings.no_results_found.toPlatformString()) - } else { - Search.Success(it.mangaList.toImmutableList()) - } - } - .catch { - log.warn(it) { "Error getting search from ${source.displayName}" } - emit(Search.Failure(it)) - } - .onEach { - results[source.id] = it - } - .collect() - } - } - }.awaitAll() - } - } - .catch { - log.warn(it) { "Error getting sources" } - } - .flowOn(Dispatchers.IO) - .launchIn(scope) - } - - fun setQuery(query: String) { - _query.value = query - } - - fun startSearch(query: String) { - search.value = query - } - - data class Params( - val initialQuery: String, - ) - - sealed class Search { - data object Searching : Search() - - data class Success( - val mangaList: ImmutableList, - ) : Search() - - data class Failure( - val e: String?, - ) : Search() { - constructor(e: Throwable) : this(e.message) + private fun getSources() { + getSourceList.asFlow() + .onEach { sources -> + installedSources.value = sources.sortedWith( + compareBy(String.CASE_INSENSITIVE_ORDER) { it.lang } + .thenBy(String.CASE_INSENSITIVE_ORDER) { + it.name + }, + ) + _isLoading.value = false } - } + .catch { + toast(it.message.orEmpty()) + log.warn(it) { "Error getting sources" } + _isLoading.value = false + } + .launchIn(scope) + } - private companion object { - private val log = logging() + private val semaphore = Semaphore(5) + + private fun readySearch() { + search + .combine(sources) { query, sources -> + query to sources + } + .mapLatest { (query, sources) -> + results.clear() + supervisorScope { + sources.map { source -> + async { + semaphore.withPermit { + getSearchManga.asFlow(source, 1, query, null) + .map { + if (it.mangaList.isEmpty()) { + Search.Failure(MR.strings.no_results_found.toPlatformString()) + } else { + Search.Success(it.mangaList.toImmutableList()) + } + } + .catch { + log.warn(it) { "Error getting search from ${source.displayName}" } + emit(Search.Failure(it)) + } + .onEach { + results[source.id] = it + } + .collect() + } + } + }.awaitAll() + } + } + .catch { + log.warn(it) { "Error getting sources" } + } + .flowOn(Dispatchers.IO) + .launchIn(scope) + } + + fun setQuery(query: String) { + _query.value = query + } + + fun startSearch(query: String) { + search.value = query + } + + data class Params( + val initialQuery: String, + ) + + sealed class Search { + data object Searching : Search() + + data class Success( + val mangaList: ImmutableList, + ) : Search() + + data class Failure( + val e: String?, + ) : Search() { + constructor(e: Throwable) : this(e.message) } } + + private companion object { + private val log = logging() + } +} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/SourceHomeScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/SourceHomeScreenViewModel.kt index cc2ad0bf..44c9575d 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/SourceHomeScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/SourceHomeScreenViewModel.kt @@ -34,100 +34,99 @@ import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class SourceHomeScreenViewModel - @Inject - constructor( - private val getSourceList: GetSourceList, - catalogPreferences: CatalogPreferences, - contextWrapper: ContextWrapper, - @Assisted private val savedStateHandle: SavedStateHandle, - ) : ViewModel(contextWrapper) { - private val _isLoading = MutableStateFlow(true) - val isLoading = _isLoading.asStateFlow() +@Inject +class SourceHomeScreenViewModel( + private val getSourceList: GetSourceList, + catalogPreferences: CatalogPreferences, + contextWrapper: ContextWrapper, + @Assisted private val savedStateHandle: SavedStateHandle, +) : ViewModel(contextWrapper) { + private val _isLoading = MutableStateFlow(true) + val isLoading = _isLoading.asStateFlow() - private val installedSources = MutableStateFlow(emptyList()) + private val installedSources = MutableStateFlow(emptyList()) - private val _languages = catalogPreferences.languages().asStateFlow() - val languages = _languages.asStateFlow() - .map { it.toImmutableSet() } - .stateIn(scope, SharingStarted.Eagerly, persistentSetOf()) + private val _languages = catalogPreferences.languages().asStateFlow() + val languages = _languages.asStateFlow() + .map { it.toImmutableSet() } + .stateIn(scope, SharingStarted.Eagerly, persistentSetOf()) - val sources = combine(installedSources, languages) { installedSources, languages -> - val all = MR.strings.all.toPlatformString() - val other = MR.strings.other.toPlatformString() - installedSources - .distinctBy { it.id } - .filter { - it.lang in languages || it.id == Source.LOCAL_SOURCE_ID + val sources = combine(installedSources, languages) { installedSources, languages -> + val all = MR.strings.all.toPlatformString() + val other = MR.strings.other.toPlatformString() + installedSources + .distinctBy { it.id } + .filter { + it.lang in languages || it.id == Source.LOCAL_SOURCE_ID + } + .groupBy(Source::displayLang) + .mapValues { + it.value.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Source::name)) + .map(SourceUI::SourceItem) + } + .mapKeys { (key) -> + when (key) { + "all" -> all + "other" -> other + else -> Locale(key).displayName } - .groupBy(Source::displayLang) - .mapValues { - it.value.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Source::name)) - .map(SourceUI::SourceItem) - } - .mapKeys { (key) -> + } + .toList() + .sortedWith( + compareBy> { (key) -> when (key) { - "all" -> all - "other" -> other - else -> Locale(key).displayName + all -> 1 + other -> 3 + else -> 2 } - } - .toList() - .sortedWith( - compareBy> { (key) -> - when (key) { - all -> 1 - other -> 3 - else -> 2 - } - }.thenBy(String.CASE_INSENSITIVE_ORDER, Pair::first), - ) - .flatMap { (key, value) -> - listOf(SourceUI.Header(key)) + value - } - .toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + }.thenBy(String.CASE_INSENSITIVE_ORDER, Pair::first), + ) + .flatMap { (key, value) -> + listOf(SourceUI.Header(key)) + value + } + .toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - val sourceLanguages = installedSources.map { sources -> - sources.map { it.lang }.distinct().minus(Source.LOCAL_SOURCE_LANG) - .toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + val sourceLanguages = installedSources.map { sources -> + sources.map { it.lang }.distinct().minus(Source.LOCAL_SOURCE_LANG) + .toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - private val _query by savedStateHandle.getStateFlow { "" } - val query = _query.asStateFlow() + private val _query by savedStateHandle.getStateFlow { "" } + val query = _query.asStateFlow() - init { - getSources() - } - - private fun getSources() { - getSourceList.asFlow() - .onEach { - installedSources.value = it - _isLoading.value = false - } - .catch { - toast(it.message.orEmpty()) - log.warn(it) { "Error getting sources" } - _isLoading.value = false - } - .launchIn(scope) - } - - fun setEnabledLanguages(langs: Set) { - log.info { langs } - _languages.value = langs - } - - fun setQuery(query: String) { - _query.value = query - } - - private companion object { - private val log = logging() - } + init { + getSources() } + private fun getSources() { + getSourceList.asFlow() + .onEach { + installedSources.value = it + _isLoading.value = false + } + .catch { + toast(it.message.orEmpty()) + log.warn(it) { "Error getting sources" } + _isLoading.value = false + } + .launchIn(scope) + } + + fun setEnabledLanguages(langs: Set) { + log.info { langs } + _languages.value = langs + } + + fun setQuery(query: String) { + _query.value = query + } + + private companion object { + private val log = logging() + } +} + @Stable sealed class SourceUI { @Stable diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/SourceSettingsScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/SourceSettingsScreenViewModel.kt index 1c41f1d6..7282e21c 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/SourceSettingsScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/SourceSettingsScreenViewModel.kt @@ -28,65 +28,64 @@ import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class SourceSettingsScreenViewModel - @Inject - constructor( - private val getSourceSettings: GetSourceSettings, - private val setSourceSetting: SetSourceSetting, - contextWrapper: ContextWrapper, - @Assisted private val params: Params, - ) : ViewModel(contextWrapper) { - private val _loading = MutableStateFlow(true) - val loading = _loading.asStateFlow() +@Inject +class SourceSettingsScreenViewModel( + private val getSourceSettings: GetSourceSettings, + private val setSourceSetting: SetSourceSetting, + contextWrapper: ContextWrapper, + @Assisted private val params: Params, +) : ViewModel(contextWrapper) { + private val _loading = MutableStateFlow(true) + val loading = _loading.asStateFlow() - private val _sourceSettings = MutableStateFlow>>(persistentListOf()) - val sourceSettings = _sourceSettings.asStateFlow() + private val _sourceSettings = MutableStateFlow>>(persistentListOf()) + val sourceSettings = _sourceSettings.asStateFlow() - init { - getSourceSettings() - sourceSettings.mapLatest { settings -> - supervisorScope { - settings.forEach { setting -> - setting.state.drop(1) - .filterNotNull() - .onEach { - setSourceSetting.await( - sourceId = params.sourceId, - setting.props, - onError = { toast(it.message.orEmpty()) }, - ) - getSourceSettings() - } - .launchIn(this) - } + init { + getSourceSettings() + sourceSettings.mapLatest { settings -> + supervisorScope { + settings.forEach { setting -> + setting.state.drop(1) + .filterNotNull() + .onEach { + setSourceSetting.await( + sourceId = params.sourceId, + setting.props, + onError = { toast(it.message.orEmpty()) }, + ) + getSourceSettings() + } + .launchIn(this) } - }.launchIn(scope) - } - - private fun getSourceSettings() { - getSourceSettings.asFlow(params.sourceId) - .onEach { - _sourceSettings.value = it.toView() - _loading.value = false - } - .catch { - toast(it.message.orEmpty()) - log.warn(it) { "Error setting source setting" } - _loading.value = false - } - .launchIn(scope) - } - - data class Params( - val sourceId: Long, - ) - - private fun List.toView() = - mapIndexed { index, sourcePreference -> - SourceSettingsView(index, sourcePreference) - }.toImmutableList() - - private companion object { - private val log = logging() - } + } + }.launchIn(scope) } + + private fun getSourceSettings() { + getSourceSettings.asFlow(params.sourceId) + .onEach { + _sourceSettings.value = it.toView() + _loading.value = false + } + .catch { + toast(it.message.orEmpty()) + log.warn(it) { "Error setting source setting" } + _loading.value = false + } + .launchIn(scope) + } + + data class Params( + val sourceId: Long, + ) + + private fun List.toView() = + mapIndexed { index, sourcePreference -> + SourceSettingsView(index, sourcePreference) + }.toImmutableList() + + private companion object { + private val log = logging() + } +} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreenViewModel.kt index 46bec1eb..83249fa4 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreenViewModel.kt @@ -40,204 +40,203 @@ import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdatesScreenViewModel - @Inject - constructor( - private val queueChapterDownload: QueueChapterDownload, - private val stopChapterDownload: StopChapterDownload, - private val deleteChapterDownload: DeleteChapterDownload, - private val getRecentUpdates: GetRecentUpdates, - private val updateChapter: UpdateChapter, - private val batchChapterDownload: BatchChapterDownload, - private val updateLibrary: UpdateLibrary, - private val updatesPager: UpdatesPager, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - private val _isLoading = MutableStateFlow(true) - val isLoading = _isLoading.asStateFlow() +@Inject +class UpdatesScreenViewModel( + private val queueChapterDownload: QueueChapterDownload, + private val stopChapterDownload: StopChapterDownload, + private val deleteChapterDownload: DeleteChapterDownload, + private val getRecentUpdates: GetRecentUpdates, + private val updateChapter: UpdateChapter, + private val batchChapterDownload: BatchChapterDownload, + private val updateLibrary: UpdateLibrary, + private val updatesPager: UpdatesPager, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + private val _isLoading = MutableStateFlow(true) + val isLoading = _isLoading.asStateFlow() - val updates = updatesPager.updates.map { updates -> - updates.map { - when (it) { - is UpdatesPager.Updates.Date -> UpdatesUI.Header(it.date) - is UpdatesPager.Updates.Update -> UpdatesUI.Item(ChapterDownloadItem(it.manga, it.chapter)) + val updates = updatesPager.updates.map { updates -> + updates.map { + when (it) { + is UpdatesPager.Updates.Date -> UpdatesUI.Header(it.date) + is UpdatesPager.Updates.Update -> UpdatesUI.Item(ChapterDownloadItem(it.manga, it.chapter)) + } + }.toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + + private val _selectedIds = MutableStateFlow>(persistentListOf()) + val selectedItems = combine(updates, _selectedIds) { updates, selectedItems -> + updates.asSequence() + .filterIsInstance() + .filter { it.chapterDownloadItem.isSelected(selectedItems) } + .map { it.chapterDownloadItem } + .toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + + val inActionMode = _selectedIds.map { it.isNotEmpty() } + .stateIn(scope, SharingStarted.Eagerly, false) + + init { + updatesPager.loadNextPage( + onComplete = { + _isLoading.value = false + }, + onError = { + toast(it.message.orEmpty()) + }, + ) + updates + .map { updates -> + updates.filterIsInstance().mapNotNull { + it.chapterDownloadItem.manga?.id + }.toSet() + } + .combine(DownloadService.downloadQueue) { mangaIds, queue -> + mangaIds to queue + } + .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + .onEach { (mangaIds, queue) -> + val chapters = queue.filter { it.mangaId in mangaIds } + updates.value.filterIsInstance().forEach { + it.chapterDownloadItem.updateFrom(chapters) } - }.toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - - private val _selectedIds = MutableStateFlow>(persistentListOf()) - val selectedItems = combine(updates, _selectedIds) { updates, selectedItems -> - updates.asSequence() - .filterIsInstance() - .filter { it.chapterDownloadItem.isSelected(selectedItems) } - .map { it.chapterDownloadItem } - .toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - - val inActionMode = _selectedIds.map { it.isNotEmpty() } - .stateIn(scope, SharingStarted.Eagerly, false) - - init { - updatesPager.loadNextPage( - onComplete = { - _isLoading.value = false - }, - onError = { - toast(it.message.orEmpty()) - }, - ) - updates - .map { updates -> - updates.filterIsInstance().mapNotNull { - it.chapterDownloadItem.manga?.id - }.toSet() - } - .combine(DownloadService.downloadQueue) { mangaIds, queue -> - mangaIds to queue - } - .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - .onEach { (mangaIds, queue) -> - val chapters = queue.filter { it.mangaId in mangaIds } - updates.value.filterIsInstance().forEach { - it.chapterDownloadItem.updateFrom(chapters) - } - } - .flowOn(Dispatchers.Default) - .launchIn(scope) - } - - fun loadNextPage() { - updatesPager.loadNextPage( - onError = { - toast(it.message.orEmpty()) - }, - ) - } - - private fun setRead( - chapterIds: List, - read: Boolean, - ) { - scope.launch { - updateChapter.await(chapterIds, read = read, onError = { toast(it.message.orEmpty()) }) - _selectedIds.value = persistentListOf() } - } + .flowOn(Dispatchers.Default) + .launchIn(scope) + } - fun markRead(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) + fun loadNextPage() { + updatesPager.loadNextPage( + onError = { + toast(it.message.orEmpty()) + }, + ) + } - fun markUnread(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) - - private fun setBookmarked( - chapterIds: List, - bookmark: Boolean, - ) { - scope.launch { - updateChapter.await(chapterIds, bookmarked = bookmark, onError = { toast(it.message.orEmpty()) }) - _selectedIds.value = persistentListOf() - } - } - - fun bookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) - - fun unBookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) - - fun downloadChapter(chapter: Chapter?) { - scope.launch { - if (chapter == null) { - val selectedIds = _selectedIds.value - batchChapterDownload.await(selectedIds, onError = { toast(it.message.orEmpty()) }) - _selectedIds.value = persistentListOf() - return@launch - } - queueChapterDownload.await(chapter.id, onError = { toast(it.message.orEmpty()) }) - } - } - - fun deleteDownloadedChapter(chapter: Chapter?) { - scope.launchDefault { - if (chapter == null) { - val selectedIds = _selectedIds.value - deleteChapterDownload.await(selectedIds, onError = { toast(it.message.orEmpty()) }) - selectedItems.value.forEach { - it.setNotDownloaded() - } - _selectedIds.value = persistentListOf() - return@launchDefault - } - updates.value - .filterIsInstance() - .find { (chapterDownloadItem) -> - chapterDownloadItem.chapter.mangaId == chapter.mangaId && - chapterDownloadItem.chapter.index == chapter.index - } - ?.chapterDownloadItem - ?.deleteDownload(deleteChapterDownload) - } - } - - fun stopDownloadingChapter(chapter: Chapter) { - scope.launchDefault { - updates.value - .filterIsInstance() - .find { (chapterDownloadItem) -> - chapterDownloadItem.chapter.mangaId == chapter.mangaId && - chapterDownloadItem.chapter.index == chapter.index - } - ?.chapterDownloadItem - ?.stopDownloading(stopChapterDownload) - } - } - - fun selectAll() { - scope.launchDefault { - _selectedIds.value = updates.value.filterIsInstance() - .map { it.chapterDownloadItem.chapter.id } - .toImmutableList() - } - } - - fun invertSelection() { - scope.launchDefault { - _selectedIds.value = updates.value.filterIsInstance() - .map { it.chapterDownloadItem.chapter.id } - .minus(_selectedIds.value) - .toImmutableList() - } - } - - fun selectChapter(id: Long) { - scope.launchDefault { - _selectedIds.value = _selectedIds.value.plus(id).toImmutableList() - } - } - - fun unselectChapter(id: Long) { - scope.launchDefault { - _selectedIds.value = _selectedIds.value.minus(id).toImmutableList() - } - } - - fun clearSelection() { - scope.launchDefault { - _selectedIds.value = persistentListOf() - } - } - - fun updateLibrary() { - scope.launchDefault { updateLibrary.await(onError = { toast(it.message.orEmpty()) }) } - } - - override fun onDispose() { - super.onDispose() - updatesPager.cancel() - } - - private companion object { - private val log = logging() + private fun setRead( + chapterIds: List, + read: Boolean, + ) { + scope.launch { + updateChapter.await(chapterIds, read = read, onError = { toast(it.message.orEmpty()) }) + _selectedIds.value = persistentListOf() } } + fun markRead(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) + + fun markUnread(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) + + private fun setBookmarked( + chapterIds: List, + bookmark: Boolean, + ) { + scope.launch { + updateChapter.await(chapterIds, bookmarked = bookmark, onError = { toast(it.message.orEmpty()) }) + _selectedIds.value = persistentListOf() + } + } + + fun bookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) + + fun unBookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) + + fun downloadChapter(chapter: Chapter?) { + scope.launch { + if (chapter == null) { + val selectedIds = _selectedIds.value + batchChapterDownload.await(selectedIds, onError = { toast(it.message.orEmpty()) }) + _selectedIds.value = persistentListOf() + return@launch + } + queueChapterDownload.await(chapter.id, onError = { toast(it.message.orEmpty()) }) + } + } + + fun deleteDownloadedChapter(chapter: Chapter?) { + scope.launchDefault { + if (chapter == null) { + val selectedIds = _selectedIds.value + deleteChapterDownload.await(selectedIds, onError = { toast(it.message.orEmpty()) }) + selectedItems.value.forEach { + it.setNotDownloaded() + } + _selectedIds.value = persistentListOf() + return@launchDefault + } + updates.value + .filterIsInstance() + .find { (chapterDownloadItem) -> + chapterDownloadItem.chapter.mangaId == chapter.mangaId && + chapterDownloadItem.chapter.index == chapter.index + } + ?.chapterDownloadItem + ?.deleteDownload(deleteChapterDownload) + } + } + + fun stopDownloadingChapter(chapter: Chapter) { + scope.launchDefault { + updates.value + .filterIsInstance() + .find { (chapterDownloadItem) -> + chapterDownloadItem.chapter.mangaId == chapter.mangaId && + chapterDownloadItem.chapter.index == chapter.index + } + ?.chapterDownloadItem + ?.stopDownloading(stopChapterDownload) + } + } + + fun selectAll() { + scope.launchDefault { + _selectedIds.value = updates.value.filterIsInstance() + .map { it.chapterDownloadItem.chapter.id } + .toImmutableList() + } + } + + fun invertSelection() { + scope.launchDefault { + _selectedIds.value = updates.value.filterIsInstance() + .map { it.chapterDownloadItem.chapter.id } + .minus(_selectedIds.value) + .toImmutableList() + } + } + + fun selectChapter(id: Long) { + scope.launchDefault { + _selectedIds.value = _selectedIds.value.plus(id).toImmutableList() + } + } + + fun unselectChapter(id: Long) { + scope.launchDefault { + _selectedIds.value = _selectedIds.value.minus(id).toImmutableList() + } + } + + fun clearSelection() { + scope.launchDefault { + _selectedIds.value = persistentListOf() + } + } + + fun updateLibrary() { + scope.launchDefault { updateLibrary.await(onError = { toast(it.message.orEmpty()) }) } + } + + override fun onDispose() { + super.onDispose() + updatesPager.cancel() + } + + private companion object { + private val log = logging() + } +} + sealed class UpdatesUI { data class Item( val chapterDownloadItem: ChapterDownloadItem, diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/main/components/TrayViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/main/components/TrayViewModel.kt index ce7ebdfc..85c8d1c9 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/main/components/TrayViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/main/components/TrayViewModel.kt @@ -18,26 +18,26 @@ import kotlinx.coroutines.flow.shareIn import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging +@Inject class TrayViewModel - @Inject - constructor( - updateChecker: UpdateChecker, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - override val scope = MainScope() +constructor( + updateChecker: UpdateChecker, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + override val scope = MainScope() - val updateFound = updateChecker - .asFlow(false) - .catch { log.warn(it) { "Failed to check for updates" } } - .filterIsInstance() - .shareIn(scope, SharingStarted.Eagerly, 1) + val updateFound = updateChecker + .asFlow(false) + .catch { log.warn(it) { "Failed to check for updates" } } + .filterIsInstance() + .shareIn(scope, SharingStarted.Eagerly, 1) - override fun onDispose() { - super.onDispose() - scope.cancel() - } - - companion object { - private val log = logging() - } + override fun onDispose() { + super.onDispose() + scope.cancel() } + + companion object { + private val log = logging() + } +} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/settings/DesktopSettingsServerScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/settings/DesktopSettingsServerScreen.kt index 23266248..13bf0b7c 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/settings/DesktopSettingsServerScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/settings/DesktopSettingsServerScreen.kt @@ -66,72 +66,72 @@ actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostVie } } +@Inject actual class SettingsServerHostViewModel - @Inject - constructor( - serverPreferences: ServerPreferences, - serverHostPreferences: ServerHostPreferences, - private val serverService: ServerService, - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - val host = serverHostPreferences.host().asStateIn(scope) +constructor( + serverPreferences: ServerPreferences, + serverHostPreferences: ServerHostPreferences, + private val serverService: ServerService, + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + val host = serverHostPreferences.host().asStateIn(scope) - // IP - val ip = serverHostPreferences.ip().asStateIn(scope) - val port = serverHostPreferences.port().asStringStateIn(scope) + // IP + val ip = serverHostPreferences.ip().asStateIn(scope) + val port = serverHostPreferences.port().asStringStateIn(scope) - // Root - val rootPath = serverHostPreferences.rootPath().asStateIn(scope) + // Root + val rootPath = serverHostPreferences.rootPath().asStateIn(scope) - // Downloader - val downloadPath = serverHostPreferences.downloadPath().asStateIn(scope) + // Downloader + val downloadPath = serverHostPreferences.downloadPath().asStateIn(scope) - // Backup - val backupPath = serverHostPreferences.backupPath().asStateIn(scope) + // Backup + val backupPath = serverHostPreferences.backupPath().asStateIn(scope) - // LocalSource - val localSourcePath = serverHostPreferences.localSourcePath().asStateIn(scope) + // LocalSource + val localSourcePath = serverHostPreferences.localSourcePath().asStateIn(scope) - // Authentication - val basicAuthEnabled = serverHostPreferences.basicAuthEnabled().asStateIn(scope) - val basicAuthUsername = serverHostPreferences.basicAuthUsername().asStateIn(scope) - val basicAuthPassword = serverHostPreferences.basicAuthPassword().asStateIn(scope) + // Authentication + val basicAuthEnabled = serverHostPreferences.basicAuthEnabled().asStateIn(scope) + val basicAuthUsername = serverHostPreferences.basicAuthUsername().asStateIn(scope) + val basicAuthPassword = serverHostPreferences.basicAuthPassword().asStateIn(scope) - private val _serverSettingChanged = MutableStateFlow(false) - val serverSettingChanged = _serverSettingChanged.asStateFlow() + private val _serverSettingChanged = MutableStateFlow(false) + val serverSettingChanged = _serverSettingChanged.asStateFlow() - fun serverSettingChanged() { - _serverSettingChanged.value = true - } + fun serverSettingChanged() { + _serverSettingChanged.value = true + } - fun restartServer() { - if (serverSettingChanged.value) { - serverService.startServer() - } - } - - // Handle password connection to hosted server - val auth = serverPreferences.auth().asStateIn(scope) - val authUsername = serverPreferences.authUsername().asStateIn(scope) - val authPassword = serverPreferences.authPassword().asStateIn(scope) - - init { - combine(host, basicAuthEnabled, basicAuthUsername, basicAuthPassword) { host, enabled, username, password -> - if (host) { - if (enabled) { - auth.value = Auth.BASIC - authUsername.value = username - authPassword.value = password - } else { - auth.value = Auth.NONE - authUsername.value = "" - authPassword.value = "" - } - } - }.launchIn(scope) + fun restartServer() { + if (serverSettingChanged.value) { + serverService.startServer() } } + // Handle password connection to hosted server + val auth = serverPreferences.auth().asStateIn(scope) + val authUsername = serverPreferences.authUsername().asStateIn(scope) + val authPassword = serverPreferences.authPassword().asStateIn(scope) + + init { + combine(host, basicAuthEnabled, basicAuthUsername, basicAuthPassword) { host, enabled, username, password -> + if (host) { + if (enabled) { + auth.value = Auth.BASIC + authUsername.value = username + authPassword.value = password + } else { + auth.value = Auth.NONE + authUsername.value = "" + authPassword.value = "" + } + } + }.launchIn(scope) + } +} + fun LazyListScope.ServerHostItems( hostValue: Boolean, basicAuthEnabledValue: Boolean, diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt index 191228b4..c7da53ff 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt @@ -11,12 +11,12 @@ import ca.gosyer.jui.uicore.vm.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import me.tatarka.inject.annotations.Inject +@Inject actual class DebugOverlayViewModel - @Inject - constructor( - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - actual val maxMemory: String - get() = "" - actual val usedMemoryFlow: MutableStateFlow = MutableStateFlow("") - } +constructor( + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + actual val maxMemory: String + get() = "" + actual val usedMemoryFlow: MutableStateFlow = MutableStateFlow("") +} diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/settings/IosSettingsServerScreen.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/settings/IosSettingsServerScreen.kt index 3f98c566..95ad494d 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/settings/IosSettingsServerScreen.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/settings/IosSettingsServerScreen.kt @@ -15,8 +15,8 @@ import me.tatarka.inject.annotations.Inject @Composable actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostViewModel): LazyListScope.() -> Unit = {} +@Inject actual class SettingsServerHostViewModel - @Inject - constructor( - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) +constructor( + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt index 665cb3d6..80951bb0 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt @@ -16,36 +16,36 @@ import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import kotlin.time.Duration.Companion.milliseconds +@Inject actual class DebugOverlayViewModel - @Inject - constructor( - contextWrapper: ContextWrapper, - ) : ViewModel(contextWrapper) { - override val scope = MainScope() +constructor( + contextWrapper: ContextWrapper, +) : ViewModel(contextWrapper) { + override val scope = MainScope() - val runtime: Runtime = Runtime.getRuntime() - actual val maxMemory = runtime.maxMemory().formatSize() - actual val usedMemoryFlow = MutableStateFlow(runtime.usedMemory().formatSize()) + val runtime: Runtime = Runtime.getRuntime() + actual val maxMemory = runtime.maxMemory().formatSize() + actual val usedMemoryFlow = MutableStateFlow(runtime.usedMemory().formatSize()) - init { - scope.launch { - while (true) { - usedMemoryFlow.value = runtime.usedMemory().formatSize() - delay(100.milliseconds) - } + init { + scope.launch { + while (true) { + usedMemoryFlow.value = runtime.usedMemory().formatSize() + delay(100.milliseconds) } } - - private fun Long.formatSize(): String { - if (this < 1024) return "$this B" - val z = (63 - java.lang.Long.numberOfLeadingZeros(this)) / 10 - return String.format("%.1f %sB", toDouble() / (1L shl z * 10), " KMGTPE"[z]) - } - - private fun Runtime.usedMemory(): Long = totalMemory() - freeMemory() - - override fun onDispose() { - super.onDispose() - scope.cancel() - } } + + private fun Long.formatSize(): String { + if (this < 1024) return "$this B" + val z = (63 - java.lang.Long.numberOfLeadingZeros(this)) / 10 + return String.format("%.1f %sB", toDouble() / (1L shl z * 10), " KMGTPE"[z]) + } + + private fun Runtime.usedMemory(): Long = totalMemory() - freeMemory() + + override fun onDispose() { + super.onDispose() + scope.cancel() + } +}