diff --git a/data/src/jvmMain/kotlin/ca/gosyer/data/models/Chapter.kt b/data/src/jvmMain/kotlin/ca/gosyer/data/models/Chapter.kt index af32a258..700483ed 100644 --- a/data/src/jvmMain/kotlin/ca/gosyer/data/models/Chapter.kt +++ b/data/src/jvmMain/kotlin/ca/gosyer/data/models/Chapter.kt @@ -7,6 +7,8 @@ package ca.gosyer.data.models import ca.gosyer.data.server.interactions.ChapterInteractionHandler +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow import kotlinx.serialization.Serializable @Serializable @@ -28,13 +30,15 @@ data class Chapter( val downloaded: Boolean, val meta: ChapterMeta ) { - suspend fun updateRemote( + fun updateRemote( chapterHandler: ChapterInteractionHandler, pageOffset: Int = meta.juiPageOffset - ) { + ) = flow { if (pageOffset != meta.juiPageOffset) { - chapterHandler.updateChapterMeta(this, "juiPageOffset", pageOffset.toString()) + chapterHandler.updateChapterMeta(this@Chapter, "juiPageOffset", pageOffset.toString()) + .collect() } + emit(Unit) } } diff --git a/data/src/jvmMain/kotlin/ca/gosyer/data/models/Manga.kt b/data/src/jvmMain/kotlin/ca/gosyer/data/models/Manga.kt index 58cf7c51..263d6719 100644 --- a/data/src/jvmMain/kotlin/ca/gosyer/data/models/Manga.kt +++ b/data/src/jvmMain/kotlin/ca/gosyer/data/models/Manga.kt @@ -9,6 +9,8 @@ package ca.gosyer.data.models import ca.gosyer.data.server.interactions.MangaInteractionHandler import ca.gosyer.i18n.MR import dev.icerock.moko.resources.StringResource +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @@ -37,10 +39,12 @@ data class Manga( suspend fun updateRemote( mangaHandler: MangaInteractionHandler, readerMode: String = meta.juiReaderMode - ) { + ) = flow { if (readerMode != meta.juiReaderMode) { - mangaHandler.updateMangaMeta(this, "juiReaderMode", readerMode) + mangaHandler.updateMangaMeta(this@Manga, "juiReaderMode", readerMode) + .collect() } + emit(Unit) } } diff --git a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/BackupInteractionHandler.kt b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/BackupInteractionHandler.kt index d25d841b..6fd289e6 100644 --- a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/BackupInteractionHandler.kt +++ b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/BackupInteractionHandler.kt @@ -6,7 +6,6 @@ package ca.gosyer.data.server.interactions -import ca.gosyer.core.lang.withIOContext import ca.gosyer.data.models.BackupValidationResult import ca.gosyer.data.server.Http import ca.gosyer.data.server.ServerPreferences @@ -21,6 +20,9 @@ import io.ktor.client.statement.HttpResponse import io.ktor.http.ContentType import io.ktor.http.Headers import io.ktor.http.HttpHeaders +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject import okio.FileSystem import okio.Path @@ -31,8 +33,8 @@ class BackupInteractionHandler @Inject constructor( serverPreferences: ServerPreferences ) : BaseInteractionHandler(client, serverPreferences) { - suspend fun importBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}) = withIOContext { - client.submitFormWithBinaryData( + fun importBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}) = flow { + val response = client.submitFormWithBinaryData( serverUrl + backupFileImportRequest(), formData = formData { append( @@ -45,10 +47,11 @@ class BackupInteractionHandler @Inject constructor( }, block = block ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun validateBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}) = withIOContext { - client.submitFormWithBinaryData( + fun validateBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}) = flow { + val response = client.submitFormWithBinaryData( serverUrl + validateBackupFileRequest(), formData = formData { append( @@ -61,12 +64,14 @@ class BackupInteractionHandler @Inject constructor( }, block = block ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun exportBackupFile(block: HttpRequestBuilder.() -> Unit = {}) = withIOContext { - client.get( + fun exportBackupFile(block: HttpRequestBuilder.() -> Unit = {}) = flow { + val response = client.get( serverUrl + backupFileExportRequest(), block ) - } + emit(response) + }.flowOn(Dispatchers.IO) } diff --git a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/CategoryInteractionHandler.kt b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/CategoryInteractionHandler.kt index 42560b66..1cc27a64 100644 --- a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/CategoryInteractionHandler.kt +++ b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/CategoryInteractionHandler.kt @@ -6,7 +6,6 @@ package ca.gosyer.data.server.interactions -import ca.gosyer.core.lang.withIOContext import ca.gosyer.data.models.Category import ca.gosyer.data.models.Manga import ca.gosyer.data.server.Http @@ -26,6 +25,9 @@ import io.ktor.client.request.get import io.ktor.client.statement.HttpResponse import io.ktor.http.HttpMethod import io.ktor.http.Parameters +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject class CategoryInteractionHandler @Inject constructor( @@ -33,53 +35,58 @@ class CategoryInteractionHandler @Inject constructor( serverPreferences: ServerPreferences ) : BaseInteractionHandler(client, serverPreferences) { - suspend fun getMangaCategories(mangaId: Long) = withIOContext { - client.get>( + fun getMangaCategories(mangaId: Long) = flow { + val response = client.get>( serverUrl + getMangaCategoriesQuery(mangaId) ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun getMangaCategories(manga: Manga) = getMangaCategories(manga.id) + fun getMangaCategories(manga: Manga) = getMangaCategories(manga.id) - suspend fun addMangaToCategory(mangaId: Long, categoryId: Long) = withIOContext { - client.get( + fun addMangaToCategory(mangaId: Long, categoryId: Long) = flow { + val response = client.get( serverUrl + addMangaToCategoryQuery(mangaId, categoryId) ) - } - suspend fun addMangaToCategory(manga: Manga, category: Category) = addMangaToCategory(manga.id, category.id) - suspend fun addMangaToCategory(manga: Manga, categoryId: Long) = addMangaToCategory(manga.id, categoryId) - suspend fun addMangaToCategory(mangaId: Long, category: Category) = addMangaToCategory(mangaId, category.id) + emit(response) + }.flowOn(Dispatchers.IO) + fun addMangaToCategory(manga: Manga, category: Category) = addMangaToCategory(manga.id, category.id) + fun addMangaToCategory(manga: Manga, categoryId: Long) = addMangaToCategory(manga.id, categoryId) + fun addMangaToCategory(mangaId: Long, category: Category) = addMangaToCategory(mangaId, category.id) - suspend fun removeMangaFromCategory(mangaId: Long, categoryId: Long) = withIOContext { - client.delete( + fun removeMangaFromCategory(mangaId: Long, categoryId: Long) = flow { + val response = client.delete( serverUrl + removeMangaFromCategoryRequest(mangaId, categoryId) ) - } - suspend fun removeMangaFromCategory(manga: Manga, category: Category) = removeMangaFromCategory(manga.id, category.id) - suspend fun removeMangaFromCategory(manga: Manga, categoryId: Long) = removeMangaFromCategory(manga.id, categoryId) - suspend fun removeMangaFromCategory(mangaId: Long, category: Category) = removeMangaFromCategory(mangaId, category.id) + emit(response) + }.flowOn(Dispatchers.IO) + fun removeMangaFromCategory(manga: Manga, category: Category) = removeMangaFromCategory(manga.id, category.id) + fun removeMangaFromCategory(manga: Manga, categoryId: Long) = removeMangaFromCategory(manga.id, categoryId) + fun removeMangaFromCategory(mangaId: Long, category: Category) = removeMangaFromCategory(mangaId, category.id) - suspend fun getCategories(dropDefault: Boolean = false) = withIOContext { - client.get>( + fun getCategories(dropDefault: Boolean = false) = flow { + val response = client.get>( serverUrl + getCategoriesQuery() ).let { categories -> if (dropDefault) { categories.filterNot { it.name.equals("default", true) } } else categories } - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun createCategory(name: String) = withIOContext { - client.submitForm( + fun createCategory(name: String) = flow { + val response = client.submitForm( serverUrl + createCategoryRequest(), formParameters = Parameters.build { append("name", name) } ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun modifyCategory(categoryId: Long, name: String? = null, isLanding: Boolean? = null) = withIOContext { - client.submitForm( + fun modifyCategory(categoryId: Long, name: String? = null, isLanding: Boolean? = null) = flow { + val response = client.submitForm( serverUrl + categoryModifyRequest(categoryId), formParameters = Parameters.build { if (name != null) { @@ -92,11 +99,12 @@ class CategoryInteractionHandler @Inject constructor( ) { method = HttpMethod.Patch } - } - suspend fun modifyCategory(category: Category, name: String? = null, isLanding: Boolean? = null) = modifyCategory(category.id, name, isLanding) + emit(response) + }.flowOn(Dispatchers.IO) + fun modifyCategory(category: Category, name: String? = null, isLanding: Boolean? = null) = modifyCategory(category.id, name, isLanding) - suspend fun reorderCategory(to: Int, from: Int) = withIOContext { - client.submitForm( + fun reorderCategory(to: Int, from: Int) = flow { + val response = client.submitForm( serverUrl + categoryReorderRequest(), formParameters = Parameters.build { append("to", to.toString()) @@ -105,19 +113,22 @@ class CategoryInteractionHandler @Inject constructor( ) { method = HttpMethod.Patch } - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun deleteCategory(categoryId: Long) = withIOContext { - client.delete( + fun deleteCategory(categoryId: Long) = flow { + val response = client.delete( serverUrl + categoryDeleteRequest(categoryId) ) - } - suspend fun deleteCategory(category: Category) = deleteCategory(category.id) + emit(response) + }.flowOn(Dispatchers.IO) + fun deleteCategory(category: Category) = deleteCategory(category.id) - suspend fun getMangaFromCategory(categoryId: Long) = withIOContext { - client.get>( + fun getMangaFromCategory(categoryId: Long) = flow { + val response = client.get>( serverUrl + getMangaInCategoryQuery(categoryId) ) - } - suspend fun getMangaFromCategory(category: Category) = getMangaFromCategory(category.id) + emit(response) + }.flowOn(Dispatchers.IO) + fun getMangaFromCategory(category: Category) = getMangaFromCategory(category.id) } diff --git a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/ChapterInteractionHandler.kt b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/ChapterInteractionHandler.kt index e4d79105..1b5da899 100644 --- a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/ChapterInteractionHandler.kt +++ b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/ChapterInteractionHandler.kt @@ -6,7 +6,6 @@ package ca.gosyer.data.server.interactions -import ca.gosyer.core.lang.withIOContext import ca.gosyer.data.models.Chapter import ca.gosyer.data.models.Manga import ca.gosyer.data.server.Http @@ -28,6 +27,9 @@ import io.ktor.client.statement.HttpResponse import io.ktor.http.HttpMethod import io.ktor.http.Parameters import io.ktor.utils.io.ByteReadChannel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject class ChapterInteractionHandler @Inject constructor( @@ -35,8 +37,8 @@ class ChapterInteractionHandler @Inject constructor( serverPreferences: ServerPreferences ) : BaseInteractionHandler(client, serverPreferences) { - suspend fun getChapters(mangaId: Long, refresh: Boolean = false) = withIOContext { - client.get>( + fun getChapters(mangaId: Long, refresh: Boolean = false) = flow { + val response = client.get>( serverUrl + getMangaChaptersQuery(mangaId) ) { url { @@ -45,31 +47,33 @@ class ChapterInteractionHandler @Inject constructor( } } } - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun getChapters(manga: Manga, refresh: Boolean = false) = getChapters(manga.id, refresh) + fun getChapters(manga: Manga, refresh: Boolean = false) = getChapters(manga.id, refresh) - suspend fun getChapter(mangaId: Long, chapterIndex: Int) = withIOContext { - client.get( + fun getChapter(mangaId: Long, chapterIndex: Int) = flow { + val response = client.get( serverUrl + getChapterQuery(mangaId, chapterIndex) ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun getChapter(chapter: Chapter) = getChapter(chapter.mangaId, chapter.index) + fun getChapter(chapter: Chapter) = getChapter(chapter.mangaId, chapter.index) - suspend fun getChapter(manga: Manga, chapterIndex: Int) = getChapter(manga.id, chapterIndex) + fun getChapter(manga: Manga, chapterIndex: Int) = getChapter(manga.id, chapterIndex) - suspend fun getChapter(manga: Manga, chapter: Chapter) = getChapter(manga.id, chapter.index) + fun getChapter(manga: Manga, chapter: Chapter) = getChapter(manga.id, chapter.index) - suspend fun updateChapter( + fun updateChapter( mangaId: Long, chapterIndex: Int, read: Boolean? = null, bookmarked: Boolean? = null, lastPageRead: Int? = null, markPreviousRead: Boolean? = null - ) = withIOContext { - client.submitForm( + ) = flow { + val response = client.submitForm( serverUrl + updateChapterRequest(mangaId, chapterIndex), formParameters = Parameters.build { if (read != null) { @@ -88,9 +92,10 @@ class ChapterInteractionHandler @Inject constructor( ) { method = HttpMethod.Patch } - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun updateChapter( + fun updateChapter( manga: Manga, chapterIndex: Int, read: Boolean? = null, @@ -106,7 +111,7 @@ class ChapterInteractionHandler @Inject constructor( markPreviousRead ) - suspend fun updateChapter( + fun updateChapter( manga: Manga, chapter: Chapter, read: Boolean? = null, @@ -122,57 +127,61 @@ class ChapterInteractionHandler @Inject constructor( markPreviousRead ) - suspend fun getPage(mangaId: Long, chapterIndex: Int, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = withIOContext { - client.get( + fun getPage(mangaId: Long, chapterIndex: Int, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = flow { + val response = client.get( serverUrl + getPageQuery(mangaId, chapterIndex, pageNum), block ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun getPage(chapter: Chapter, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = getPage(chapter.mangaId, chapter.index, pageNum, block) + fun getPage(chapter: Chapter, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = getPage(chapter.mangaId, chapter.index, pageNum, block) - suspend fun getPage(manga: Manga, chapterIndex: Int, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = getPage(manga.id, chapterIndex, pageNum, block) + fun getPage(manga: Manga, chapterIndex: Int, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = getPage(manga.id, chapterIndex, pageNum, block) - suspend fun getPage(manga: Manga, chapter: Chapter, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = getPage(manga.id, chapter.index, pageNum, block) + fun getPage(manga: Manga, chapter: Chapter, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = getPage(manga.id, chapter.index, pageNum, block) - suspend fun deleteChapterDownload(mangaId: Long, chapterIndex: Int) = withIOContext { - client.delete( + fun deleteChapterDownload(mangaId: Long, chapterIndex: Int) = flow { + val response = client.delete( serverUrl + deleteDownloadedChapterRequest(mangaId, chapterIndex) ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun deleteChapterDownload(chapter: Chapter) = deleteChapterDownload(chapter.mangaId, chapter.index) + fun deleteChapterDownload(chapter: Chapter) = deleteChapterDownload(chapter.mangaId, chapter.index) - suspend fun deleteChapterDownload(manga: Manga, chapterIndex: Int) = deleteChapterDownload(manga.id, chapterIndex) + fun deleteChapterDownload(manga: Manga, chapterIndex: Int) = deleteChapterDownload(manga.id, chapterIndex) - suspend fun deleteChapterDownload(manga: Manga, chapter: Chapter) = deleteChapterDownload(manga.id, chapter.index) + fun deleteChapterDownload(manga: Manga, chapter: Chapter) = deleteChapterDownload(manga.id, chapter.index) - suspend fun queueChapterDownload(mangaId: Long, chapterIndex: Int) = withIOContext { - client.get( + fun queueChapterDownload(mangaId: Long, chapterIndex: Int) = flow { + val response = client.get( serverUrl + queueDownloadChapterRequest(mangaId, chapterIndex) ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun queueChapterDownload(chapter: Chapter) = queueChapterDownload(chapter.mangaId, chapter.index) + fun queueChapterDownload(chapter: Chapter) = queueChapterDownload(chapter.mangaId, chapter.index) - suspend fun queueChapterDownload(manga: Manga, chapterIndex: Int) = queueChapterDownload(manga.id, chapterIndex) + fun queueChapterDownload(manga: Manga, chapterIndex: Int) = queueChapterDownload(manga.id, chapterIndex) - suspend fun queueChapterDownload(manga: Manga, chapter: Chapter) = queueChapterDownload(manga.id, chapter.index) + fun queueChapterDownload(manga: Manga, chapter: Chapter) = queueChapterDownload(manga.id, chapter.index) - suspend fun stopChapterDownload(mangaId: Long, chapterIndex: Int) = withIOContext { - client.delete( + fun stopChapterDownload(mangaId: Long, chapterIndex: Int) = flow { + val response = client.delete( serverUrl + stopDownloadingChapterRequest(mangaId, chapterIndex) ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun stopChapterDownload(chapter: Chapter) = stopChapterDownload(chapter.mangaId, chapter.index) + fun stopChapterDownload(chapter: Chapter) = stopChapterDownload(chapter.mangaId, chapter.index) - suspend fun stopChapterDownload(manga: Manga, chapterIndex: Int) = stopChapterDownload(manga.id, chapterIndex) + fun stopChapterDownload(manga: Manga, chapterIndex: Int) = stopChapterDownload(manga.id, chapterIndex) - suspend fun stopChapterDownload(manga: Manga, chapter: Chapter) = stopChapterDownload(manga.id, chapter.index) + fun stopChapterDownload(manga: Manga, chapter: Chapter) = stopChapterDownload(manga.id, chapter.index) - suspend fun updateChapterMeta(mangaId: Long, chapterIndex: Int, key: String, value: String) = withIOContext { - client.submitForm( + fun updateChapterMeta(mangaId: Long, chapterIndex: Int, key: String, value: String) = flow { + val response = client.submitForm( serverUrl + updateChapterMetaRequest(mangaId, chapterIndex), formParameters = Parameters.build { append("key", key) @@ -181,11 +190,12 @@ class ChapterInteractionHandler @Inject constructor( ) { method = HttpMethod.Patch } - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun updateChapterMeta(chapter: Chapter, key: String, value: String) = updateChapterMeta(chapter.mangaId, chapter.index, key, value) + fun updateChapterMeta(chapter: Chapter, key: String, value: String) = updateChapterMeta(chapter.mangaId, chapter.index, key, value) - suspend fun updateChapterMeta(manga: Manga, chapterIndex: Int, key: String, value: String) = updateChapterMeta(manga.id, chapterIndex, key, value) + fun updateChapterMeta(manga: Manga, chapterIndex: Int, key: String, value: String) = updateChapterMeta(manga.id, chapterIndex, key, value) - suspend fun updateChapterMeta(manga: Manga, chapter: Chapter, key: String, value: String) = updateChapterMeta(manga.id, chapter.index, key, value) + fun updateChapterMeta(manga: Manga, chapter: Chapter, key: String, value: String) = updateChapterMeta(manga.id, chapter.index, key, value) } diff --git a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/DownloadInteractionHandler.kt b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/DownloadInteractionHandler.kt index fb2069e2..9a092bde 100644 --- a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/DownloadInteractionHandler.kt +++ b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/DownloadInteractionHandler.kt @@ -6,7 +6,6 @@ package ca.gosyer.data.server.interactions -import ca.gosyer.core.lang.withIOContext import ca.gosyer.data.server.Http import ca.gosyer.data.server.ServerPreferences import ca.gosyer.data.server.requests.downloadsClearRequest @@ -14,6 +13,9 @@ import ca.gosyer.data.server.requests.downloadsStartRequest import ca.gosyer.data.server.requests.downloadsStopRequest import io.ktor.client.request.get import io.ktor.client.statement.HttpResponse +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject class DownloadInteractionHandler @Inject constructor( @@ -21,21 +23,24 @@ class DownloadInteractionHandler @Inject constructor( serverPreferences: ServerPreferences ) : BaseInteractionHandler(client, serverPreferences) { - suspend fun startDownloading() = withIOContext { - client.get( + fun startDownloading() = flow { + val response = client.get( serverUrl + downloadsStartRequest() ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun stopDownloading() = withIOContext { - client.get( + fun stopDownloading() = flow { + val response = client.get( serverUrl + downloadsStopRequest() ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun clearDownloadQueue() = withIOContext { - client.get( + fun clearDownloadQueue() = flow { + val response = client.get( serverUrl + downloadsClearRequest() ) - } + emit(response) + }.flowOn(Dispatchers.IO) } diff --git a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/ExtensionInteractionHandler.kt b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/ExtensionInteractionHandler.kt index 49232963..bca5fefa 100644 --- a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/ExtensionInteractionHandler.kt +++ b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/ExtensionInteractionHandler.kt @@ -6,7 +6,6 @@ package ca.gosyer.data.server.interactions -import ca.gosyer.core.lang.withIOContext import ca.gosyer.data.models.Extension import ca.gosyer.data.server.Http import ca.gosyer.data.server.ServerPreferences @@ -19,6 +18,9 @@ import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.get import io.ktor.client.statement.HttpResponse import io.ktor.utils.io.ByteReadChannel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject class ExtensionInteractionHandler @Inject constructor( @@ -26,34 +28,39 @@ class ExtensionInteractionHandler @Inject constructor( serverPreferences: ServerPreferences ) : BaseInteractionHandler(client, serverPreferences) { - suspend fun getExtensionList() = withIOContext { - client.get>( + fun getExtensionList() = flow { + val response = client.get>( serverUrl + extensionListQuery() ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun installExtension(extension: Extension) = withIOContext { - client.get( + fun installExtension(extension: Extension) = flow { + val response = client.get( serverUrl + apkInstallQuery(extension.pkgName) ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun updateExtension(extension: Extension) = withIOContext { - client.get( + fun updateExtension(extension: Extension) = flow { + val response = client.get( serverUrl + apkUpdateQuery(extension.pkgName) ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun uninstallExtension(extension: Extension) = withIOContext { - client.get( + fun uninstallExtension(extension: Extension) = flow { + val response = client.get( serverUrl + apkUninstallQuery(extension.pkgName) ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun getApkIcon(extension: Extension, block: HttpRequestBuilder.() -> Unit) = withIOContext { - client.get( + fun getApkIcon(extension: Extension, block: HttpRequestBuilder.() -> Unit) = flow { + val response = client.get( serverUrl + apkIconQuery(extension.apkName), block ) - } + emit(response) + }.flowOn(Dispatchers.IO) } diff --git a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/LibraryInteractionHandler.kt b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/LibraryInteractionHandler.kt index da2a9775..c29634c3 100644 --- a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/LibraryInteractionHandler.kt +++ b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/LibraryInteractionHandler.kt @@ -6,7 +6,6 @@ package ca.gosyer.data.server.interactions -import ca.gosyer.core.lang.withIOContext import ca.gosyer.data.models.Manga import ca.gosyer.data.server.Http import ca.gosyer.data.server.ServerPreferences @@ -15,6 +14,9 @@ import ca.gosyer.data.server.requests.removeMangaFromLibraryRequest import io.ktor.client.request.delete import io.ktor.client.request.get import io.ktor.client.statement.HttpResponse +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject class LibraryInteractionHandler @Inject constructor( @@ -22,19 +24,21 @@ class LibraryInteractionHandler @Inject constructor( serverPreferences: ServerPreferences ) : BaseInteractionHandler(client, serverPreferences) { - suspend fun addMangaToLibrary(mangaId: Long) = withIOContext { - client.get( + fun addMangaToLibrary(mangaId: Long) = flow { + val response = client.get( serverUrl + addMangaToLibraryQuery(mangaId) ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun addMangaToLibrary(manga: Manga) = addMangaToLibrary(manga.id) + fun addMangaToLibrary(manga: Manga) = addMangaToLibrary(manga.id) - suspend fun removeMangaFromLibrary(mangaId: Long) = withIOContext { - client.delete( + fun removeMangaFromLibrary(mangaId: Long) = flow { + val response = client.delete( serverUrl + removeMangaFromLibraryRequest(mangaId) ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun removeMangaFromLibrary(manga: Manga) = removeMangaFromLibrary(manga.id) + fun removeMangaFromLibrary(manga: Manga) = removeMangaFromLibrary(manga.id) } diff --git a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/MangaInteractionHandler.kt b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/MangaInteractionHandler.kt index 2b9b24c0..62ab42b6 100644 --- a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/MangaInteractionHandler.kt +++ b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/MangaInteractionHandler.kt @@ -6,7 +6,6 @@ package ca.gosyer.data.server.interactions -import ca.gosyer.core.lang.withIOContext import ca.gosyer.data.models.Manga import ca.gosyer.data.server.Http import ca.gosyer.data.server.ServerPreferences @@ -21,6 +20,9 @@ import io.ktor.client.statement.HttpResponse import io.ktor.http.HttpMethod import io.ktor.http.Parameters import io.ktor.utils.io.ByteReadChannel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject class MangaInteractionHandler @Inject constructor( @@ -28,8 +30,8 @@ class MangaInteractionHandler @Inject constructor( serverPreferences: ServerPreferences ) : BaseInteractionHandler(client, serverPreferences) { - suspend fun getManga(mangaId: Long, refresh: Boolean = false) = withIOContext { - client.get( + fun getManga(mangaId: Long, refresh: Boolean = false) = flow { + val response = client.get( serverUrl + mangaQuery(mangaId) ) { url { @@ -38,19 +40,21 @@ class MangaInteractionHandler @Inject constructor( } } } - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun getManga(manga: Manga, refresh: Boolean = false) = getManga(manga.id, refresh) + fun getManga(manga: Manga, refresh: Boolean = false) = getManga(manga.id, refresh) - suspend fun getMangaThumbnail(mangaId: Long, block: HttpRequestBuilder.() -> Unit) = withIOContext { - client.get( + fun getMangaThumbnail(mangaId: Long, block: HttpRequestBuilder.() -> Unit) = flow { + val response = client.get( serverUrl + mangaThumbnailQuery(mangaId), block ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun updateMangaMeta(mangaId: Long, key: String, value: String) = withIOContext { - client.submitForm( + fun updateMangaMeta(mangaId: Long, key: String, value: String) = flow { + val response = client.submitForm( serverUrl + updateMangaMetaRequest(mangaId), formParameters = Parameters.build { append("key", key) @@ -59,7 +63,8 @@ class MangaInteractionHandler @Inject constructor( ) { method = HttpMethod.Patch } - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun updateMangaMeta(manga: Manga, key: String, value: String) = updateMangaMeta(manga.id, key, value) + fun updateMangaMeta(manga: Manga, key: String, value: String) = updateMangaMeta(manga.id, key, value) } diff --git a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/SourceInteractionHandler.kt b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/SourceInteractionHandler.kt index 29fe0ffe..977509df 100644 --- a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/SourceInteractionHandler.kt +++ b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/SourceInteractionHandler.kt @@ -6,7 +6,6 @@ package ca.gosyer.data.server.interactions -import ca.gosyer.core.lang.withIOContext import ca.gosyer.data.models.MangaPage import ca.gosyer.data.models.Source import ca.gosyer.data.models.sourcefilters.SourceFilter @@ -31,6 +30,9 @@ import io.ktor.client.request.post import io.ktor.client.statement.HttpResponse import io.ktor.http.ContentType import io.ktor.http.contentType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import me.tatarka.inject.annotations.Inject @@ -40,45 +42,49 @@ class SourceInteractionHandler @Inject constructor( serverPreferences: ServerPreferences ) : BaseInteractionHandler(client, serverPreferences) { - suspend fun getSourceList() = withIOContext { - client.get>( + fun getSourceList() = flow { + val response = client.get>( serverUrl + sourceListQuery() ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun getSourceInfo(sourceId: Long) = withIOContext { - client.get( + fun getSourceInfo(sourceId: Long) = flow { + val response = client.get( serverUrl + sourceInfoQuery(sourceId) ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun getSourceInfo(source: Source) = getSourceInfo(source.id) + fun getSourceInfo(source: Source) = getSourceInfo(source.id) - suspend fun getPopularManga(sourceId: Long, pageNum: Int) = withIOContext { - client.get( + fun getPopularManga(sourceId: Long, pageNum: Int) = flow { + val response = client.get( serverUrl + sourcePopularQuery(sourceId, pageNum) ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun getPopularManga(source: Source, pageNum: Int) = getPopularManga( + fun getPopularManga(source: Source, pageNum: Int) = getPopularManga( source.id, pageNum ) - suspend fun getLatestManga(sourceId: Long, pageNum: Int) = withIOContext { - client.get( + fun getLatestManga(sourceId: Long, pageNum: Int) = flow { + val response = client.get( serverUrl + sourceLatestQuery(sourceId, pageNum) ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun getLatestManga(source: Source, pageNum: Int) = getLatestManga( + fun getLatestManga(source: Source, pageNum: Int) = getLatestManga( source.id, pageNum ) // TODO: 2021-03-14 - suspend fun getGlobalSearchResults(searchTerm: String) = withIOContext { - client.get( + fun getGlobalSearchResults(searchTerm: String) = flow { + val response = client.get( serverUrl + globalSearchQuery() ) { url { @@ -87,10 +93,11 @@ class SourceInteractionHandler @Inject constructor( } } } - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun getSearchResults(sourceId: Long, searchTerm: String, pageNum: Int) = withIOContext { - client.get( + fun getSearchResults(sourceId: Long, searchTerm: String, pageNum: Int) = flow { + val response = client.get( serverUrl + sourceSearchQuery(sourceId) ) { url { @@ -100,16 +107,17 @@ class SourceInteractionHandler @Inject constructor( } } } - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun getSearchResults(source: Source, searchTerm: String, pageNum: Int) = getSearchResults( + fun getSearchResults(source: Source, searchTerm: String, pageNum: Int) = getSearchResults( source.id, searchTerm, pageNum ) - suspend fun getFilterList(sourceId: Long, reset: Boolean = false) = withIOContext { - client.get>( + fun getFilterList(sourceId: Long, reset: Boolean = false) = flow { + val response = client.get>( serverUrl + getFilterListQuery(sourceId) ) { url { @@ -118,25 +126,27 @@ class SourceInteractionHandler @Inject constructor( } } } - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun getFilterList(source: Source, reset: Boolean = false) = getFilterList(source.id, reset) + fun getFilterList(source: Source, reset: Boolean = false) = getFilterList(source.id, reset) - suspend fun setFilter(sourceId: Long, sourceFilter: SourceFilterChange) = withIOContext { - client.post( + fun setFilter(sourceId: Long, sourceFilter: SourceFilterChange) = flow { + val response = client.post( serverUrl + setFilterRequest(sourceId) ) { contentType(ContentType.Application.Json) body = sourceFilter } - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun setFilter(sourceId: Long, position: Int, value: Any) = setFilter( + fun setFilter(sourceId: Long, position: Int, value: Any) = setFilter( sourceId, SourceFilterChange(position, value) ) - suspend fun setFilter(sourceId: Long, parentPosition: Int, childPosition: Int, value: Any) = setFilter( + fun setFilter(sourceId: Long, parentPosition: Int, childPosition: Int, value: Any) = setFilter( sourceId, SourceFilterChange( parentPosition, @@ -144,24 +154,26 @@ class SourceInteractionHandler @Inject constructor( ) ) - suspend fun getSourceSettings(sourceId: Long) = withIOContext { - client.get>( + fun getSourceSettings(sourceId: Long) = flow { + val response = client.get>( serverUrl + getSourceSettingsQuery(sourceId) ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun getSourceSettings(source: Source) = getSourceSettings(source.id) + fun getSourceSettings(source: Source) = getSourceSettings(source.id) - suspend fun setSourceSetting(sourceId: Long, sourcePreference: SourcePreferenceChange) = withIOContext { - client.post( + fun setSourceSetting(sourceId: Long, sourcePreference: SourcePreferenceChange) = flow { + val response = client.post( serverUrl + updateSourceSettingQuery(sourceId) ) { contentType(ContentType.Application.Json) body = sourcePreference } - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun setSourceSetting(sourceId: Long, position: Int, value: Any) = setSourceSetting( + fun setSourceSetting(sourceId: Long, position: Int, value: Any) = setSourceSetting( sourceId, SourcePreferenceChange(position, value) ) diff --git a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/UpdatesInteractionHandler.kt b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/UpdatesInteractionHandler.kt index 40f2143e..3c05ff4b 100644 --- a/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/UpdatesInteractionHandler.kt +++ b/data/src/jvmMain/kotlin/ca/gosyer/data/server/interactions/UpdatesInteractionHandler.kt @@ -6,7 +6,6 @@ package ca.gosyer.data.server.interactions -import ca.gosyer.core.lang.withIOContext import ca.gosyer.data.models.Category import ca.gosyer.data.models.Updates import ca.gosyer.data.server.Http @@ -18,6 +17,9 @@ import io.ktor.client.request.get import io.ktor.client.request.post import io.ktor.client.statement.HttpResponse import io.ktor.http.Parameters +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject class UpdatesInteractionHandler @Inject constructor( @@ -25,26 +27,29 @@ class UpdatesInteractionHandler @Inject constructor( serverPreferences: ServerPreferences ) : BaseInteractionHandler(client, serverPreferences) { - suspend fun getRecentUpdates(pageNum: Int) = withIOContext { - client.get( + fun getRecentUpdates(pageNum: Int) = flow { + val response = client.get( serverUrl + recentUpdatesQuery(pageNum) ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun updateLibrary() = withIOContext { - client.post( + fun updateLibrary() = flow { + val response = client.post( serverUrl + fetchUpdatesRequest() ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun updateCategory(categoryId: Long) = withIOContext { - client.submitForm( + fun updateCategory(categoryId: Long) = flow { + val response = client.submitForm( serverUrl + fetchUpdatesRequest(), formParameters = Parameters.build { append("category", categoryId.toString()) } ) - } + emit(response) + }.flowOn(Dispatchers.IO) - suspend fun updateCategory(category: Category) = updateCategory(category.id) + fun updateCategory(category: Category) = updateCategory(category.id) } diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/chapter/ChapterDownloadButtons.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/chapter/ChapterDownloadButtons.kt index 22c8b470..0ceed8e2 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/chapter/ChapterDownloadButtons.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/chapter/ChapterDownloadButtons.kt @@ -40,8 +40,11 @@ import ca.gosyer.data.server.interactions.ChapterInteractionHandler import ca.gosyer.i18n.MR import ca.gosyer.uicore.components.DropdownIconButton import ca.gosyer.uicore.resources.stringResource +import io.ktor.client.statement.HttpResponse +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.onEach data class ChapterDownloadItem( val manga: Manga?, @@ -71,14 +74,18 @@ data class ChapterDownloadItem( _downloadChapterFlow.value = downloadingChapter } - suspend fun deleteDownload(chapterHandler: ChapterInteractionHandler) { - chapterHandler.deleteChapterDownload(chapter) - _downloadState.value = ChapterDownloadState.NotDownloaded + fun deleteDownload(chapterHandler: ChapterInteractionHandler): Flow { + return chapterHandler.deleteChapterDownload(chapter) + .onEach { + _downloadState.value = ChapterDownloadState.NotDownloaded + } } - suspend fun stopDownloading(chapterHandler: ChapterInteractionHandler) { - chapterHandler.stopChapterDownload(chapter) - _downloadState.value = ChapterDownloadState.NotDownloaded + fun stopDownloading(chapterHandler: ChapterInteractionHandler): Flow { + return chapterHandler.stopChapterDownload(chapter) + .onEach { + _downloadState.value = ChapterDownloadState.NotDownloaded + } } } diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/categories/CategoriesScreenViewModel.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/categories/CategoriesScreenViewModel.kt index ecc5a88f..7f7fe717 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/categories/CategoriesScreenViewModel.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/categories/CategoriesScreenViewModel.kt @@ -6,7 +6,6 @@ package ca.gosyer.ui.categories -import ca.gosyer.core.lang.throwIfCancellation import ca.gosyer.core.logging.CKLogger import ca.gosyer.data.models.Category import ca.gosyer.data.server.interactions.CategoryInteractionHandler @@ -14,7 +13,11 @@ import ca.gosyer.uicore.vm.ContextWrapper import ca.gosyer.uicore.vm.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject class CategoriesScreenViewModel @Inject constructor( @@ -32,21 +35,22 @@ class CategoriesScreenViewModel @Inject constructor( getCategories() } - fun getCategories() { - scope.launch { - _categories.value = emptyList() - _isLoading.value = true - try { - _categories.value = categoryHandler.getCategories(true) + private fun getCategories() { + _categories.value = emptyList() + _isLoading.value = true + categoryHandler.getCategories(true) + .onEach { + _categories.value = it .sortedBy { it.order } .also { originalCategories = it } .map { it.toMenuCategory() } - } catch (e: Exception) { - e.throwIfCancellation() - } finally { _isLoading.value = false } - } + .catch { + info(it) { "Error getting categories" } + _isLoading.value = false + } + .launchIn(scope) } suspend fun updateRemoteCategories(manualUpdate: Boolean = false) { @@ -54,23 +58,47 @@ class CategoriesScreenViewModel @Inject constructor( val newCategories = categories.filter { it.id == null } newCategories.forEach { categoryHandler.createCategory(it.name) + .catch { + info(it) { "Error creating category" } + } + .collect() } originalCategories.forEach { originalCategory -> val category = categories.find { it.id == originalCategory.id } if (category == null) { categoryHandler.deleteCategory(originalCategory) + .catch { + info(it) { "Error deleting category $originalCategory" } + } + .collect() } else if (category.name != originalCategory.name) { categoryHandler.modifyCategory(originalCategory, category.name) + .catch { + info(it) { "Error modifying category $category" } + } + .collect() } } var updatedCategories = categoryHandler.getCategories(true) + .catch { + info(it) { "Error getting updated categories" } + } + .singleOrNull() categories.forEach { category -> - val updatedCategory = updatedCategories.find { it.id == category.id || it.name == category.name } ?: return@forEach + val updatedCategory = updatedCategories?.find { it.id == category.id || it.name == category.name } ?: return@forEach if (category.order != updatedCategory.order) { debug { "${category.name}: ${updatedCategory.order} to ${category.order}" } categoryHandler.reorderCategory(category.order, updatedCategory.order) + .catch { + info(it) { "Error re-ordering categories" } + } + .singleOrNull() } updatedCategories = categoryHandler.getCategories(true) + .catch { + info(it) { "Error getting updated categories" } + } + .singleOrNull() } if (manualUpdate) { diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreenViewModel.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreenViewModel.kt index 759908bd..6cf02b02 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreenViewModel.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreenViewModel.kt @@ -6,6 +6,7 @@ package ca.gosyer.ui.downloads +import ca.gosyer.core.logging.CKLogger import ca.gosyer.data.download.DownloadService import ca.gosyer.data.models.Chapter import ca.gosyer.data.server.interactions.ChapterInteractionHandler @@ -14,7 +15,10 @@ import ca.gosyer.uicore.vm.ContextWrapper import ca.gosyer.uicore.vm.ViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject class DownloadsScreenViewModel @Inject constructor( @@ -36,35 +40,53 @@ class DownloadsScreenViewModel @Inject constructor( val downloadQueue get() = downloadService.downloadQueue fun start() { - scope.launch { - downloadsHandler.startDownloading() - } + downloadsHandler.startDownloading() + .catch { + info(it) { "Error starting download" } + } + .launchIn(scope) } fun pause() { - scope.launch { - downloadsHandler.stopDownloading() - } + downloadsHandler.stopDownloading() + .catch { + info(it) { "Error stopping download" } + } + .launchIn(scope) } fun clear() { - scope.launch { - downloadsHandler.clearDownloadQueue() - } + downloadsHandler.clearDownloadQueue() + .catch { + info(it) { "Error clearing download" } + } + .launchIn(scope) } fun stopDownload(chapter: Chapter) { - scope.launch { - chapterHandler.stopChapterDownload(chapter) - } + chapterHandler.stopChapterDownload(chapter) + .catch { + info(it) { "Error stop chapter download" } + } + .launchIn(scope) } fun moveToBottom(chapter: Chapter) { - scope.launch { - chapterHandler.stopChapterDownload(chapter) - chapterHandler.queueChapterDownload(chapter) - } + chapterHandler.stopChapterDownload(chapter) + .onEach { + chapterHandler.queueChapterDownload(chapter) + .catch { + info(it) { "Error adding download" } + } + .collect() + } + .catch { + info(it) { "Error stop chapter download" } + } + .launchIn(scope) } fun restartDownloader() = downloadService.init() + + private companion object : CKLogger({}) } diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreenViewModel.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreenViewModel.kt index b024b1b0..dbcf00c8 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreenViewModel.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/extensions/ExtensionsScreenViewModel.kt @@ -6,7 +6,6 @@ package ca.gosyer.ui.extensions -import ca.gosyer.core.lang.throwIfCancellation import ca.gosyer.core.logging.CKLogger import ca.gosyer.data.extension.ExtensionPreferences import ca.gosyer.data.models.Extension @@ -17,11 +16,13 @@ import ca.gosyer.uicore.vm.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import java.util.Locale @@ -55,57 +56,61 @@ class ExtensionsScreenViewModel @Inject constructor( init { - scope.launch { - getExtensions() - } + getExtensions() } - private suspend fun getExtensions() { - try { - _isLoading.value = true - extensionList.value = extensionHandler.getExtensionList() - } catch (e: Exception) { - e.throwIfCancellation() - extensionList.value = emptyList() - } finally { - _isLoading.value = false - } + private fun getExtensions() { + extensionHandler.getExtensionList() + .onEach { + extensionList.value = it + _isLoading.value = false + } + .catch { + info(it) { "Error getting extensions" } + emit(emptyList()) + _isLoading.value = false + } + .launchIn(scope) } fun install(extension: Extension) { info { "Install clicked" } - scope.launch { - try { - extensionHandler.installExtension(extension) - } catch (e: Exception) { - e.throwIfCancellation() + extensionHandler.installExtension(extension) + .onEach { + getExtensions() } - getExtensions() - } + .catch { + info(it) { "Error installing extension ${extension.apkName}" } + getExtensions() + } + .launchIn(scope) + } fun update(extension: Extension) { info { "Update clicked" } - scope.launch { - try { - extensionHandler.updateExtension(extension) - } catch (e: Exception) { - e.throwIfCancellation() + extensionHandler.updateExtension(extension) + .onEach { + getExtensions() } - getExtensions() - } + .catch { + info(it) { "Error updating extension ${extension.apkName}" } + getExtensions() + } + .launchIn(scope) } fun uninstall(extension: Extension) { info { "Uninstall clicked" } - scope.launch { - try { - extensionHandler.uninstallExtension(extension) - } catch (e: Exception) { - e.throwIfCancellation() + extensionHandler.uninstallExtension(extension) + .onEach { + getExtensions() } - getExtensions() - } + .catch { + info(it) { "Error uninstalling extension ${extension.apkName}" } + getExtensions() + } + .launchIn(scope) } fun setEnabledLanguages(langs: Set) { diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/library/LibraryScreenViewModel.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/library/LibraryScreenViewModel.kt index 284d199f..cebb81b6 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/library/LibraryScreenViewModel.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/library/LibraryScreenViewModel.kt @@ -6,8 +6,8 @@ package ca.gosyer.ui.library -import ca.gosyer.core.lang.throwIfCancellation import ca.gosyer.core.lang.withDefaultContext +import ca.gosyer.core.logging.CKLogger import ca.gosyer.data.library.LibraryPreferences import ca.gosyer.data.models.Category import ca.gosyer.data.models.Manga @@ -24,11 +24,13 @@ import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.single import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject private typealias CategoryItems = Pair>, MutableStateFlow>> @@ -101,22 +103,22 @@ class LibraryScreenViewModel @Inject constructor( } private fun getLibrary() { - scope.launch { - _isLoading.value = true - try { - val categories = categoryHandler.getCategories() + _isLoading.value = true + categoryHandler.getCategories() + .onEach { categories -> if (categories.isEmpty()) { throw Exception("Library is empty") } library.categories.value = categories.sortedBy { it.order } updateCategories(categories) - } catch (e: Exception) { - e.throwIfCancellation() - _error.value = e.message - } finally { _isLoading.value = false } - } + .catch { + _error.value = it.message + info(it) { "Error getting categories" } + _isLoading.value = false + } + .launchIn(scope) } fun setSelectedPage(page: Int) { @@ -129,9 +131,18 @@ class LibraryScreenViewModel @Inject constructor( private suspend fun updateCategories(categories: List) { withDefaultContext { - categories.map { + categories.map { category -> async { - library.mangaMap.setManga(query.value, it.id, categoryHandler.getMangaFromCategory(it)) + library.mangaMap.setManga( + query.value, + category.id, + categoryHandler.getMangaFromCategory(category) + .catch { + info { "Error getting manga for category $category" } + emit(emptyList()) + } + .single() + ) } }.awaitAll() } @@ -146,10 +157,14 @@ class LibraryScreenViewModel @Inject constructor( } fun removeManga(mangaId: Long) { - scope.launch { - libraryHandler.removeMangaFromLibrary(mangaId) - updateCategories(getCategoriesToUpdate(mangaId)) - } + libraryHandler.removeMangaFromLibrary(mangaId) + .onEach { + updateCategories(getCategoriesToUpdate(mangaId)) + } + .catch { + info(it) { "Error removing manga from library" } + } + .launchIn(scope) } fun updateQuery(query: String) { @@ -157,19 +172,20 @@ class LibraryScreenViewModel @Inject constructor( } fun updateLibrary() { - scope.launch { - updatesHandler.updateLibrary() - } + updatesHandler.updateLibrary() + .catch { + info(it) { "Error updating library" } + } + .launchIn(scope) } fun updateCategory(category: Category) { - scope.launch { - updatesHandler.updateCategory(category) - } + updatesHandler.updateCategory(category) + .catch { + info(it) { "Error updating category" } + } + .launchIn(scope) } - companion object { - const val QUERY_KEY = "query" - const val SELECTED_CATEGORY_KEY = "selected_category" - } + private companion object : CKLogger({}) } diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/manga/MangaScreenViewModel.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/manga/MangaScreenViewModel.kt index 13278f97..e73c6c3d 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/manga/MangaScreenViewModel.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/manga/MangaScreenViewModel.kt @@ -6,8 +6,8 @@ package ca.gosyer.ui.manga -import ca.gosyer.core.lang.throwIfCancellation import ca.gosyer.core.lang.withIOContext +import ca.gosyer.core.logging.CKLogger import ca.gosyer.data.download.DownloadService import ca.gosyer.data.models.Category import ca.gosyer.data.models.Chapter @@ -25,9 +25,13 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.single import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject @@ -86,9 +90,14 @@ class MangaScreenViewModel @Inject constructor( _isLoading.value = false } - scope.launch { - _categories.value = categoryHandler.getCategories(true) - } + categoryHandler.getCategories(true) + .onEach { + _categories.value = it + } + .catch { + info(it) { "Error getting categories" } + } + .launchIn(scope) } fun loadManga() { @@ -124,22 +133,34 @@ class MangaScreenViewModel @Inject constructor( private suspend fun refreshMangaAsync(mangaId: Long, refresh: Boolean = false) = withIOContext { async { - try { - _manga.value = mangaHandler.getManga(mangaId, refresh) - _mangaCategories.value = categoryHandler.getMangaCategories(mangaId) - } catch (e: Exception) { - e.throwIfCancellation() - } + mangaHandler.getManga(mangaId, refresh) + .onEach { + _manga.value = it + } + .catch { + info(it) { "Error getting manga" } + } + .collect() + categoryHandler.getMangaCategories(mangaId) + .onEach { + _mangaCategories.value = it + } + .catch { + info(it) { "Error getting manga" } + } + .collect() } } private suspend fun refreshChaptersAsync(mangaId: Long, refresh: Boolean = false) = withIOContext { async { - try { - _chapters.value = chapterHandler.getChapters(mangaId, refresh).toDownloadChapters() - } catch (e: Exception) { - e.throwIfCancellation() - } + _chapters.value = chapterHandler.getChapters(mangaId, refresh) + .catch { + info(it) { "Error getting chapters" } + emit(emptyList()) + } + .single() + .toDownloadChapters() } } @@ -148,6 +169,10 @@ class MangaScreenViewModel @Inject constructor( manga.value?.let { manga -> if (manga.inLibrary) { libraryHandler.removeMangaFromLibrary(manga) + .catch { + info(it) { "Error toggling favorite" } + } + .collect() refreshMangaAsync(manga.id).await() } else { if (categories.value.isEmpty()) { @@ -166,12 +191,24 @@ class MangaScreenViewModel @Inject constructor( if (manga.inLibrary) { oldCategories.filterNot { it in categories }.forEach { categoryHandler.removeMangaFromCategory(manga, it) + .catch { + info(it) { "Error removing manga from category" } + } + .collect() } } else { libraryHandler.addMangaToLibrary(manga) + .catch { + info(it) { "Error Adding manga to library" } + } + .collect() } categories.filterNot { it in oldCategories }.forEach { categoryHandler.addMangaToCategory(manga, it) + .catch { + info(it) { "Error adding manga to category" } + } + .collect() } refreshMangaAsync(manga.id).await() } @@ -189,8 +226,22 @@ class MangaScreenViewModel @Inject constructor( fun toggleRead(index: Int) { scope.launch { manga.value?.let { manga -> - chapterHandler.updateChapter(manga, index, read = !_chapters.value.first { it.chapter.index == index }.chapter.read) - _chapters.value = chapterHandler.getChapters(manga).toDownloadChapters() + chapterHandler.updateChapter( + manga, + index, + read = !_chapters.value.first { it.chapter.index == index }.chapter.read + ) + .catch { + info(it) { "Error toggling read" } + } + .collect() + _chapters.value = chapterHandler.getChapters(manga) + .catch { + info(it) { "Error getting new chapters after toggling read" } + emit(emptyList()) + } + .single() + .toDownloadChapters() } } } @@ -198,8 +249,22 @@ class MangaScreenViewModel @Inject constructor( fun toggleBookmarked(index: Int) { scope.launch { manga.value?.let { manga -> - chapterHandler.updateChapter(manga, index, bookmarked = !_chapters.value.first { it.chapter.index == index }.chapter.bookmarked) - _chapters.value = chapterHandler.getChapters(manga).toDownloadChapters() + chapterHandler.updateChapter( + manga, + index, + bookmarked = !_chapters.value.first { it.chapter.index == index }.chapter.bookmarked + ) + .catch { + info(it) { "Error toggling bookmarked" } + } + .collect() + _chapters.value = chapterHandler.getChapters(manga) + .catch { + info(it) { "Error getting new chapters after toggling bookmarked" } + emit(emptyList()) + } + .single() + .toDownloadChapters() } } } @@ -208,29 +273,48 @@ class MangaScreenViewModel @Inject constructor( scope.launch { manga.value?.let { manga -> chapterHandler.updateChapter(manga, index, markPreviousRead = true) - _chapters.value = chapterHandler.getChapters(manga).toDownloadChapters() + .catch { + info(it) { "Error marking previous as read" } + } + .collect() + _chapters.value = chapterHandler.getChapters(manga) + .catch { + info(it) { "Error getting new chapters after marking previous as read" } + emit(emptyList()) + } + .single() + .toDownloadChapters() } } } fun downloadChapter(index: Int) { - scope.launch { - manga.value?.let { manga -> - chapterHandler.queueChapterDownload(manga, index) - } + manga.value?.let { manga -> + chapterHandler.queueChapterDownload(manga, index) + .catch { + info(it) { "Error downloading chapter" } + } + .launchIn(scope) } } fun deleteDownload(index: Int) { - scope.launch { - chapters.value.find { it.chapter.index == index }?.deleteDownload(chapterHandler) - } + chapters.value.find { it.chapter.index == index } + ?.deleteDownload(chapterHandler) + ?.catch { + info(it) { "Error deleting download" } + } + ?.launchIn(scope) + } fun stopDownloadingChapter(index: Int) { - scope.launch { - chapters.value.find { it.chapter.index == index }?.stopDownloading(chapterHandler) - } + chapters.value.find { it.chapter.index == index } + ?.stopDownloading(chapterHandler) + ?.catch { + info(it) { "Error stopping download" } + } + ?.launchIn(scope) } override fun onDispose() { @@ -242,4 +326,6 @@ class MangaScreenViewModel @Inject constructor( } data class Params(val mangaId: Long) + + private companion object : CKLogger({}) } diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/reader/ReaderMenuViewModel.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/reader/ReaderMenuViewModel.kt index 3e004b86..52515346 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/reader/ReaderMenuViewModel.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/reader/ReaderMenuViewModel.kt @@ -7,7 +7,6 @@ package ca.gosyer.ui.reader import ca.gosyer.core.lang.launchDefault -import ca.gosyer.core.lang.throwIfCancellation import ca.gosyer.core.logging.CKLogger import ca.gosyer.core.prefs.getAsFlow import ca.gosyer.data.models.Chapter @@ -34,10 +33,14 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.flow.singleOrNull import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject @@ -150,10 +153,15 @@ class ReaderMenuViewModel @Inject constructor( fun setMangaReaderMode(mode: String) { scope.launchDefault { - _manga.value?.updateRemote( - mangaHandler, - mode - ) + _manga.value + ?.updateRemote( + mangaHandler, + mode + ) + ?.catch { + info(it) { "Error updating manga reader mode" } + } + ?.collect() initManga(params.mangaId) } } @@ -185,35 +193,37 @@ class ReaderMenuViewModel @Inject constructor( } private suspend fun initManga(mangaId: Long) { - try { - _manga.value = mangaHandler.getManga(mangaId) - } catch (e: Exception) { - e.throwIfCancellation() - _state.value = ReaderChapter.State.Error(e) - throw e - } + mangaHandler.getManga(mangaId) + .onEach { + _manga.value = it + } + .catch { + _state.value = ReaderChapter.State.Error(it) + info(it) { "Error loading manga" } + } + .collect() } private suspend fun initChapters(mangaId: Long, chapterIndex: Int) { resetValues() val chapter = ReaderChapter( - try { - chapterHandler.getChapter(mangaId, chapterIndex) - } catch (e: Exception) { - e.throwIfCancellation() - _state.value = ReaderChapter.State.Error(e) - throw e - } + chapterHandler.getChapter(mangaId, chapterIndex) + .catch { + _state.value = ReaderChapter.State.Error(it) + info(it) { "Error getting chapter" } + } + .singleOrNull() ?: return ) val pages = loader.loadChapter(chapter) viewerChapters.currChapter.value = chapter scope.launchDefault { - val chapters = try { - chapterHandler.getChapters(mangaId) - } catch (e: Exception) { - e.throwIfCancellation() - emptyList() - } + val chapters = chapterHandler.getChapters(mangaId) + .catch { + info(it) { "Error getting chapter list" } + emit(emptyList()) + } + .single() + val nextChapter = chapters.find { it.index == chapterIndex + 1 } if (nextChapter != null) { viewerChapters.nextChapter.value = ReaderChapter( @@ -259,17 +269,23 @@ class ReaderMenuViewModel @Inject constructor( .launchIn(chapter.scope) } - private suspend fun markChapterRead(mangaId: Long, chapter: ReaderChapter) { + private fun markChapterRead(mangaId: Long, chapter: ReaderChapter) { chapterHandler.updateChapter(mangaId, chapter.chapter.index, true) + .catch { + info(it) { "Error marking chapter read" } + } + .launchIn(scope) } @OptIn(DelicateCoroutinesApi::class) fun sendProgress(chapter: Chapter? = this.chapter.value?.chapter, lastPageRead: Int = currentPage.value) { chapter ?: return if (chapter.read) return - GlobalScope.launchDefault { - chapterHandler.updateChapter(chapter.mangaId, chapter.index, lastPageRead = lastPageRead) - } + chapterHandler.updateChapter(chapter.mangaId, chapter.index, lastPageRead = lastPageRead) + .catch { + info(it) { "Error sending progress" } + } + .launchIn(GlobalScope) } fun updateLastPageReadOffset(offset: Int) { @@ -278,9 +294,11 @@ class ReaderMenuViewModel @Inject constructor( @OptIn(DelicateCoroutinesApi::class) private fun updateLastPageReadOffset(chapter: Chapter, offset: Int) { - GlobalScope.launchDefault { - chapter.updateRemote(chapterHandler, offset) - } + chapter.updateRemote(chapterHandler, offset) + .catch { + info(it) { "Error updating chapter offset" } + } + .launchIn(GlobalScope) } override fun onDispose() { diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/reader/loader/TachideskPageLoader.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/reader/loader/TachideskPageLoader.kt index 8bd388ed..4aba2fdc 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/reader/loader/TachideskPageLoader.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/reader/loader/TachideskPageLoader.kt @@ -22,6 +22,10 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicInteger @@ -58,24 +62,30 @@ class TachideskPageLoader( val page = priorityPage.page debug { "Loading page ${page.index}" } if (page.status.value == ReaderPage.Status.QUEUE) { - try { - page.bitmap.value = chapterHandler.getPage(chapter.chapter, page.index) { + chapterHandler + .getPage(chapter.chapter, page.index) { onDownload { bytesSentTotal, contentLength -> page.progress.value = (bytesSentTotal.toFloat() / contentLength).coerceAtMost(1.0F) } - }.toImageBitmap() - page.status.value = ReaderPage.Status.READY - page.error.value = null - } catch (e: Exception) { - e.throwIfCancellation() - page.bitmap.value = null - page.status.value = ReaderPage.Status.ERROR - page.error.value = e.message - } + } + .onEach { + page.bitmap.value = it.toImageBitmap() + page.status.value = ReaderPage.Status.READY + page.error.value = null + } + .catch { + page.bitmap.value = null + page.status.value = ReaderPage.Status.ERROR + page.error.value = it.message + info(it) { "Error getting image" } + } + .flowOn(Dispatchers.IO) + .collect() } } } catch (e: Exception) { e.throwIfCancellation() + info(e) { "Error in loop" } } } } diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/settings/SettingsBackupScreen.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/settings/SettingsBackupScreen.kt index 7cd40737..c03a08ee 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/settings/SettingsBackupScreen.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/settings/SettingsBackupScreen.kt @@ -66,6 +66,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -125,23 +129,33 @@ class SettingsBackupViewModel @Inject constructor( val createFlow = _createFlow.asSharedFlow() fun restoreFile(source: Source) { scope.launch { - try { + val file = try { FileSystem.SYSTEM_TEMPORARY_DIRECTORY .resolve("tachidesk.${Random.nextLong()}.proto.gz") .also { file -> source.saveTo(file) - val (missingSources) = backupHandler.validateBackupFile(file) - if (missingSources.isEmpty()) { - restoreBackup(file) - } else { - _missingSourceFlow.emit(file to missingSources) - } } } catch (e: Exception) { - info(e) { "Error importing backup" } + info(e) { "Error creating backup file" } _restoreStatus.value = Status.Error e.throwIfCancellation() + null } + file ?: return@launch + + backupHandler.validateBackupFile(file) + .onEach { (missingSources) -> + if (missingSources.isEmpty()) { + restoreBackup(file) + } else { + _missingSourceFlow.emit(file to missingSources) + } + } + .catch { + info(it) { "Error importing backup" } + _restoreStatus.value = Status.Error + } + .collect() } } @@ -150,20 +164,20 @@ class SettingsBackupViewModel @Inject constructor( _restoreStatus.value = Status.Nothing _restoringProgress.value = null _restoring.value = true - try { - backupHandler.importBackupFile(file) { - onUpload { bytesSentTotal, contentLength -> - _restoringProgress.value = (bytesSentTotal.toFloat() / contentLength).coerceAtMost(1.0F) - } + backupHandler.importBackupFile(file) { + onUpload { bytesSentTotal, contentLength -> + _restoringProgress.value = (bytesSentTotal.toFloat() / contentLength).coerceAtMost(1.0F) } - _restoreStatus.value = Status.Success - } catch (e: Exception) { - info(e) { "Error importing backup" } - _restoreStatus.value = Status.Error - e.throwIfCancellation() - } finally { - _restoring.value = false } + .onEach { + _restoreStatus.value = Status.Success + } + .catch { + info(it) { "Error importing backup" } + _restoreStatus.value = Status.Error + } + .collect() + _restoring.value = false } } @@ -176,46 +190,47 @@ class SettingsBackupViewModel @Inject constructor( private val mutex = Mutex() fun exportBackup() { - scope.launch { - _creatingStatus.value = Status.Nothing - _creatingProgress.value = null - _creating.value = true - val backup = try { - backupHandler.exportBackupFile { - onDownload { bytesSentTotal, contentLength -> - _creatingProgress.value = (bytesSentTotal.toFloat() / contentLength).coerceAtMost(0.99F) - } + _creatingStatus.value = Status.Nothing + _creatingProgress.value = null + _creating.value = true + backupHandler + .exportBackupFile { + onDownload { bytesSentTotal, contentLength -> + _creatingProgress.value = (bytesSentTotal.toFloat() / contentLength).coerceAtMost(0.99F) } - } catch (e: Exception) { - info(e) { "Error exporting backup" } - _creatingStatus.value = Status.Error - _creating.value = false - e.throwIfCancellation() - null } - _creatingProgress.value = 1.0F - if (backup != null && backup.status.isSuccess()) { - val filename = backup.headers["content-disposition"]?.substringAfter("filename=")?.trim('"') ?: "backup" - tempFile.value = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(filename).also { - launch { - try { - backup.content.toInputStream() - .source() - .saveTo(it) - } catch (e: Exception) { - e.throwIfCancellation() - error(e) { "Error creating backup" } - _creatingStatus.value = Status.Error - _creating.value = false - } finally { - mutex.unlock() + .onEach { backup -> + if (backup.status.isSuccess()) { + val filename = + backup.headers["content-disposition"]?.substringAfter("filename=") + ?.trim('"') ?: "backup" + tempFile.value = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(filename).also { + mutex.tryLock() + scope.launch { + try { + backup.content.toInputStream() + .source() + .saveTo(it) + } catch (e: Exception) { + e.throwIfCancellation() + info(e) { "Error creating backup" } + _creatingStatus.value = Status.Error + _creating.value = false + } finally { + mutex.unlock() + } } } + _createFlow.emit(filename) } - mutex.tryLock() - _createFlow.emit(filename) } - } + .catch { + info(it) { "Error exporting backup" } + _creatingStatus.value = Status.Error + _creating.value = false + } + .launchIn(scope) + } fun exportBackupFileFound(backupSink: Sink) { diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/settings/SettingsLibraryScreen.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/settings/SettingsLibraryScreen.kt index e904c463..4817d7e9 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/settings/SettingsLibraryScreen.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/settings/SettingsLibraryScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import ca.gosyer.core.logging.CKLogger import ca.gosyer.data.library.LibraryPreferences import ca.gosyer.data.server.interactions.CategoryInteractionHandler import ca.gosyer.i18n.MR @@ -39,7 +40,9 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject class SettingsLibraryScreen : Screen { @@ -72,10 +75,17 @@ class SettingsLibraryViewModel @Inject constructor( } fun refreshCategoryCount() { - scope.launch { - _categories.value = categoryHandler.getCategories(true).size - } + categoryHandler.getCategories(true) + .onEach { + _categories.value = it.size + } + .catch { + info(it) { "Error getting categories" } + } + .launchIn(scope) } + + private companion object : CKLogger({}) } @Composable diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/browse/SourceScreenViewModel.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/browse/SourceScreenViewModel.kt index 2b42c804..f457c5b3 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/browse/SourceScreenViewModel.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/browse/SourceScreenViewModel.kt @@ -6,7 +6,7 @@ package ca.gosyer.ui.sources.browse -import ca.gosyer.core.lang.throwIfCancellation +import ca.gosyer.core.logging.CKLogger import ca.gosyer.data.models.Manga import ca.gosyer.data.models.MangaPage import ca.gosyer.data.models.Source @@ -15,7 +15,10 @@ import ca.gosyer.uicore.vm.ContextWrapper import ca.gosyer.uicore.vm.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.singleOrNull import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex import me.tatarka.inject.annotations.Inject class SourceScreenViewModel( @@ -59,36 +62,33 @@ class SourceScreenViewModel( private val _pageNum = MutableStateFlow(1) val pageNum = _pageNum.asStateFlow() + private val sourceMutex = Mutex() + init { scope.launch { - try { - val (mangas, hasNextPage) = getPage() + getPage()?.let { (mangas, hasNextPage) -> _mangas.value = mangas _hasNextPage.value = hasNextPage - } catch (e: Exception) { - e.throwIfCancellation() - } finally { - _loading.value = false } + + _loading.value = false } } fun loadNextPage() { scope.launch { - val hasNextPage = hasNextPage.value - val pageNum = pageNum.value - try { - _hasNextPage.value = false + if (hasNextPage.value && sourceMutex.tryLock()) { _pageNum.value++ val page = getPage() - _mangas.value += page.mangaList - _hasNextPage.value = page.hasNextPage - } catch (e: Exception) { - _hasNextPage.value = hasNextPage - _pageNum.value = pageNum - } finally { - _loading.value = false + if (page != null) { + _mangas.value += page.mangaList + _hasNextPage.value = page.hasNextPage + } else { + _pageNum.value-- + } + sourceMutex.unlock() } + _loading.value = false } } @@ -104,12 +104,16 @@ class SourceScreenViewModel( } } - private suspend fun getPage(): MangaPage { + private suspend fun getPage(): MangaPage? { return when { isLatest.value -> sourceHandler.getLatestManga(source, pageNum.value) _query.value != null || _usingFilters.value -> sourceHandler.getSearchResults(source, _query.value.orEmpty(), pageNum.value) else -> sourceHandler.getPopularManga(source, pageNum.value) } + .catch { + info(it) { "Error getting source page" } + } + .singleOrNull() } fun startSearch(query: String?) { @@ -136,4 +140,6 @@ class SourceScreenViewModel( } data class Params(val source: Source) + + private companion object : CKLogger({}) } diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/browse/filter/SourceFiltersViewModel.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/browse/filter/SourceFiltersViewModel.kt index 532dccf9..ad979b4a 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/browse/filter/SourceFiltersViewModel.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/browse/filter/SourceFiltersViewModel.kt @@ -6,7 +6,6 @@ package ca.gosyer.ui.sources.browse.filter -import ca.gosyer.core.lang.throwIfCancellation import ca.gosyer.core.logging.CKLogger import ca.gosyer.data.models.sourcefilters.SourceFilter import ca.gosyer.data.server.interactions.SourceInteractionHandler @@ -15,12 +14,13 @@ import ca.gosyer.uicore.vm.ContextWrapper import ca.gosyer.uicore.vm.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import me.tatarka.inject.annotations.Inject @@ -69,6 +69,7 @@ class SourceFiltersViewModel( childFilter.index, it ) + .collect() getFilters() } .launchIn(this) @@ -77,13 +78,18 @@ class SourceFiltersViewModel( filter.state.drop(1).filterNotNull() .onEach { sourceHandler.setFilter(sourceId, filter.index, it) + .collect() getFilters() } .launchIn(this) } } } - }.launchIn(scope) + } + .catch { + info(it) { "Error with filters" } + } + .launchIn(scope) } fun showingFilters(show: Boolean) { @@ -91,21 +97,20 @@ class SourceFiltersViewModel( } private fun getFilters(initialLoad: Boolean = false) { - scope.launch { - try { - _filters.value = sourceHandler.getFilterList(sourceId, reset = initialLoad).toView() - } catch (e: Exception) { - e.throwIfCancellation() - } finally { + sourceHandler.getFilterList(sourceId, reset = initialLoad) + .onEach { + _filters.value = it.toView() _loading.value = false } - } + .catch { + info(it) { "Error getting filters" } + _loading.value = false + } + .launchIn(scope) } fun resetFilters() { - scope.launch { - getFilters(initialLoad = true) - } + getFilters(initialLoad = true) } data class Params(val sourceId: Long) diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreenViewModel.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreenViewModel.kt index 0702b3f2..7e910241 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreenViewModel.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/home/SourceHomeScreenViewModel.kt @@ -6,7 +6,6 @@ package ca.gosyer.ui.sources.home -import ca.gosyer.core.lang.throwIfCancellation import ca.gosyer.core.logging.CKLogger import ca.gosyer.data.catalog.CatalogPreferences import ca.gosyer.data.models.Source @@ -16,10 +15,12 @@ import ca.gosyer.uicore.vm.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject class SourceHomeScreenViewModel @Inject constructor( @@ -51,15 +52,16 @@ class SourceHomeScreenViewModel @Inject constructor( } private fun getSources() { - scope.launch { - try { - installedSources.value = sourceHandler.getSourceList() - } catch (e: Exception) { - e.throwIfCancellation() - } finally { + sourceHandler.getSourceList() + .onEach { + installedSources.value = it _isLoading.value = false } - } + .catch { + info(it) { "Error getting sources" } + _isLoading.value = false + } + .launchIn(scope) } fun setEnabledLanguages(langs: Set) { diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsScreenViewModel.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsScreenViewModel.kt index 71eed29a..53d27a79 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsScreenViewModel.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsScreenViewModel.kt @@ -6,7 +6,7 @@ package ca.gosyer.ui.sources.settings -import ca.gosyer.core.lang.throwIfCancellation +import ca.gosyer.core.logging.CKLogger import ca.gosyer.data.models.sourcepreference.SourcePreference import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.ui.sources.settings.model.SourceSettingsView @@ -14,12 +14,13 @@ import ca.gosyer.uicore.vm.ContextWrapper import ca.gosyer.uicore.vm.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import me.tatarka.inject.annotations.Inject @@ -43,6 +44,10 @@ class SourceSettingsScreenViewModel @Inject constructor( .filterNotNull() .onEach { sourceHandler.setSourceSetting(params.sourceId, setting.index, it) + .catch { + info(it) { "Error setting source setting" } + } + .collect() getSourceSettings() } .launchIn(this) @@ -53,15 +58,16 @@ class SourceSettingsScreenViewModel @Inject constructor( } private fun getSourceSettings() { - scope.launch { - try { - _sourceSettings.value = sourceHandler.getSourceSettings(params.sourceId).toView() - } catch (e: Exception) { - e.throwIfCancellation() - } finally { + sourceHandler.getSourceSettings(params.sourceId) + .onEach { + _sourceSettings.value = it.toView() _loading.value = false } - } + .catch { + info(it) { "Error setting source setting" } + _loading.value = false + } + .launchIn(scope) } data class Params(val sourceId: Long) @@ -69,4 +75,6 @@ class SourceSettingsScreenViewModel @Inject constructor( private fun List.toView() = mapIndexed { index, sourcePreference -> SourceSettingsView(index, sourcePreference) } + + private companion object : CKLogger({}) } diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/updates/UpdatesScreenViewModel.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/updates/UpdatesScreenViewModel.kt index d9b8aa4d..c96c5b3d 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/updates/UpdatesScreenViewModel.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/updates/UpdatesScreenViewModel.kt @@ -6,7 +6,7 @@ package ca.gosyer.ui.updates -import ca.gosyer.core.lang.throwIfCancellation +import ca.gosyer.core.logging.CKLogger import ca.gosyer.data.download.DownloadService import ca.gosyer.data.models.Chapter import ca.gosyer.data.server.interactions.ChapterInteractionHandler @@ -16,6 +16,8 @@ import ca.gosyer.uicore.vm.ContextWrapper import ca.gosyer.uicore.vm.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach @@ -45,77 +47,89 @@ class UpdatesScreenViewModel @Inject constructor( init { scope.launch { - try { - getUpdates(1) - } catch (e: Exception) { - e.throwIfCancellation() - } finally { - _isLoading.value = false - } + getUpdates() } } fun loadNextPage() { scope.launch { if (hasNextPage.value && updatesMutex.tryLock()) { - try { - getUpdates(currentPage.value++) - } catch (e: Exception) { - e.throwIfCancellation() - currentPage.value-- - } + getUpdates() updatesMutex.unlock() } } } - private suspend fun getUpdates(pageNum: Int) { - val updates = updatesHandler.getRecentUpdates(pageNum) - mangaIds = updates.page.map { it.manga.id }.toSet() + private suspend fun getUpdates() { + updatesHandler.getRecentUpdates(currentPage.value) + .onEach { updates -> + mangaIds = updates.page.map { it.manga.id }.toSet() - _updates.value += updates.page.map { - ChapterDownloadItem( - it.manga, - it.chapter - ) - } - downloadService.registerWatches(mangaIds).merge() - .onEach { (mangaId, chapters) -> - _updates.value.filter { it.chapter.mangaId == mangaId } - .forEach { - it.updateFrom(chapters) + _updates.value += updates.page.map { + ChapterDownloadItem( + it.manga, + it.chapter + ) + } + downloadService.registerWatches(mangaIds).merge() + .onEach { (mangaId, chapters) -> + _updates.value.filter { it.chapter.mangaId == mangaId } + .forEach { + it.updateFrom(chapters) + } } - } - .launchIn(scope) + .launchIn(scope) - hasNextPage.value = updates.hasNextPage + hasNextPage.value = updates.hasNextPage + _isLoading.value = false + } + .catch { + info(it) { "Error getting updates" } + if (currentPage.value > 1) { + currentPage.value-- + } + _isLoading.value = false + } + .collect() } fun downloadChapter(chapter: Chapter) { - scope.launch { - chapterHandler.queueChapterDownload(chapter) - } + chapterHandler.queueChapterDownload(chapter) + .catch { + info(it) { "Error queueing chapter" } + } + .launchIn(scope) } fun deleteDownloadedChapter(chapter: Chapter) { - scope.launch { - updates.value.find { + updates.value + .find { it.chapter.mangaId == chapter.mangaId && it.chapter.index == chapter.index - }?.deleteDownload(chapterHandler) - } + } + ?.deleteDownload(chapterHandler) + ?.catch { + info(it) { "Error deleting download" } + } + ?.launchIn(scope) } fun stopDownloadingChapter(chapter: Chapter) { - scope.launch { - updates.value.find { + updates.value + .find { it.chapter.mangaId == chapter.mangaId && it.chapter.index == chapter.index - }?.stopDownloading(chapterHandler) - } + } + ?.stopDownloading(chapterHandler) + ?.catch { + info(it) { "Error stopping download" } + } + ?.launchIn(scope) } override fun onDispose() { downloadService.removeWatches(mangaIds) } + + private companion object : CKLogger({}) }