Rewrite Tachidesk interactions

- Fix crashing on android when things fail to load
- Improve error handling
This commit is contained in:
Syer10
2022-03-01 13:31:50 -05:00
parent 0e65265ed0
commit a2247cfa1e
26 changed files with 844 additions and 520 deletions

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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<HttpResponse>(
fun importBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}) = flow {
val response = client.submitFormWithBinaryData<HttpResponse>(
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<BackupValidationResult>(
fun validateBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}) = flow {
val response = client.submitFormWithBinaryData<BackupValidationResult>(
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<HttpResponse>(
fun exportBackupFile(block: HttpRequestBuilder.() -> Unit = {}) = flow {
val response = client.get<HttpResponse>(
serverUrl + backupFileExportRequest(),
block
)
}
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -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<List<Category>>(
fun getMangaCategories(mangaId: Long) = flow {
val response = client.get<List<Category>>(
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<HttpResponse>(
fun addMangaToCategory(mangaId: Long, categoryId: Long) = flow {
val response = client.get<HttpResponse>(
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<HttpResponse>(
fun removeMangaFromCategory(mangaId: Long, categoryId: Long) = flow {
val response = client.delete<HttpResponse>(
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<List<Category>>(
fun getCategories(dropDefault: Boolean = false) = flow {
val response = client.get<List<Category>>(
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<HttpResponse>(
fun createCategory(name: String) = flow {
val response = client.submitForm<HttpResponse>(
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<HttpResponse>(
fun modifyCategory(categoryId: Long, name: String? = null, isLanding: Boolean? = null) = flow {
val response = client.submitForm<HttpResponse>(
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<HttpResponse>(
fun reorderCategory(to: Int, from: Int) = flow {
val response = client.submitForm<HttpResponse>(
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<HttpResponse>(
fun deleteCategory(categoryId: Long) = flow {
val response = client.delete<HttpResponse>(
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<List<Manga>>(
fun getMangaFromCategory(categoryId: Long) = flow {
val response = client.get<List<Manga>>(
serverUrl + getMangaInCategoryQuery(categoryId)
)
}
suspend fun getMangaFromCategory(category: Category) = getMangaFromCategory(category.id)
emit(response)
}.flowOn(Dispatchers.IO)
fun getMangaFromCategory(category: Category) = getMangaFromCategory(category.id)
}

View File

@@ -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<List<Chapter>>(
fun getChapters(mangaId: Long, refresh: Boolean = false) = flow {
val response = client.get<List<Chapter>>(
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<Chapter>(
fun getChapter(mangaId: Long, chapterIndex: Int) = flow {
val response = client.get<Chapter>(
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<HttpResponse>(
) = flow {
val response = client.submitForm<HttpResponse>(
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<ByteReadChannel>(
fun getPage(mangaId: Long, chapterIndex: Int, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = flow {
val response = client.get<ByteReadChannel>(
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<HttpResponse>(
fun deleteChapterDownload(mangaId: Long, chapterIndex: Int) = flow {
val response = client.delete<HttpResponse>(
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<HttpResponse>(
fun queueChapterDownload(mangaId: Long, chapterIndex: Int) = flow {
val response = client.get<HttpResponse>(
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<HttpResponse>(
fun stopChapterDownload(mangaId: Long, chapterIndex: Int) = flow {
val response = client.delete<HttpResponse>(
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<HttpResponse>(
fun updateChapterMeta(mangaId: Long, chapterIndex: Int, key: String, value: String) = flow {
val response = client.submitForm<HttpResponse>(
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)
}

View File

@@ -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<HttpResponse>(
fun startDownloading() = flow {
val response = client.get<HttpResponse>(
serverUrl + downloadsStartRequest()
)
}
emit(response)
}.flowOn(Dispatchers.IO)
suspend fun stopDownloading() = withIOContext {
client.get<HttpResponse>(
fun stopDownloading() = flow {
val response = client.get<HttpResponse>(
serverUrl + downloadsStopRequest()
)
}
emit(response)
}.flowOn(Dispatchers.IO)
suspend fun clearDownloadQueue() = withIOContext {
client.get<HttpResponse>(
fun clearDownloadQueue() = flow {
val response = client.get<HttpResponse>(
serverUrl + downloadsClearRequest()
)
}
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -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<List<Extension>>(
fun getExtensionList() = flow {
val response = client.get<List<Extension>>(
serverUrl + extensionListQuery()
)
}
emit(response)
}.flowOn(Dispatchers.IO)
suspend fun installExtension(extension: Extension) = withIOContext {
client.get<HttpResponse>(
fun installExtension(extension: Extension) = flow {
val response = client.get<HttpResponse>(
serverUrl + apkInstallQuery(extension.pkgName)
)
}
emit(response)
}.flowOn(Dispatchers.IO)
suspend fun updateExtension(extension: Extension) = withIOContext {
client.get<HttpResponse>(
fun updateExtension(extension: Extension) = flow {
val response = client.get<HttpResponse>(
serverUrl + apkUpdateQuery(extension.pkgName)
)
}
emit(response)
}.flowOn(Dispatchers.IO)
suspend fun uninstallExtension(extension: Extension) = withIOContext {
client.get<HttpResponse>(
fun uninstallExtension(extension: Extension) = flow {
val response = client.get<HttpResponse>(
serverUrl + apkUninstallQuery(extension.pkgName)
)
}
emit(response)
}.flowOn(Dispatchers.IO)
suspend fun getApkIcon(extension: Extension, block: HttpRequestBuilder.() -> Unit) = withIOContext {
client.get<ByteReadChannel>(
fun getApkIcon(extension: Extension, block: HttpRequestBuilder.() -> Unit) = flow {
val response = client.get<ByteReadChannel>(
serverUrl + apkIconQuery(extension.apkName),
block
)
}
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -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<HttpResponse>(
fun addMangaToLibrary(mangaId: Long) = flow {
val response = client.get<HttpResponse>(
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<HttpResponse>(
fun removeMangaFromLibrary(mangaId: Long) = flow {
val response = client.delete<HttpResponse>(
serverUrl + removeMangaFromLibraryRequest(mangaId)
)
}
emit(response)
}.flowOn(Dispatchers.IO)
suspend fun removeMangaFromLibrary(manga: Manga) = removeMangaFromLibrary(manga.id)
fun removeMangaFromLibrary(manga: Manga) = removeMangaFromLibrary(manga.id)
}

View File

@@ -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<Manga>(
fun getManga(mangaId: Long, refresh: Boolean = false) = flow {
val response = client.get<Manga>(
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<ByteReadChannel>(
fun getMangaThumbnail(mangaId: Long, block: HttpRequestBuilder.() -> Unit) = flow {
val response = client.get<ByteReadChannel>(
serverUrl + mangaThumbnailQuery(mangaId),
block
)
}
emit(response)
}.flowOn(Dispatchers.IO)
suspend fun updateMangaMeta(mangaId: Long, key: String, value: String) = withIOContext {
client.submitForm<HttpResponse>(
fun updateMangaMeta(mangaId: Long, key: String, value: String) = flow {
val response = client.submitForm<HttpResponse>(
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)
}

View File

@@ -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<List<Source>>(
fun getSourceList() = flow {
val response = client.get<List<Source>>(
serverUrl + sourceListQuery()
)
}
emit(response)
}.flowOn(Dispatchers.IO)
suspend fun getSourceInfo(sourceId: Long) = withIOContext {
client.get<Source>(
fun getSourceInfo(sourceId: Long) = flow {
val response = client.get<Source>(
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<MangaPage>(
fun getPopularManga(sourceId: Long, pageNum: Int) = flow {
val response = client.get<MangaPage>(
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<MangaPage>(
fun getLatestManga(sourceId: Long, pageNum: Int) = flow {
val response = client.get<MangaPage>(
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<HttpResponse>(
fun getGlobalSearchResults(searchTerm: String) = flow {
val response = client.get<HttpResponse>(
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<MangaPage>(
fun getSearchResults(sourceId: Long, searchTerm: String, pageNum: Int) = flow {
val response = client.get<MangaPage>(
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<List<SourceFilter>>(
fun getFilterList(sourceId: Long, reset: Boolean = false) = flow {
val response = client.get<List<SourceFilter>>(
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<HttpResponse>(
fun setFilter(sourceId: Long, sourceFilter: SourceFilterChange) = flow {
val response = client.post<HttpResponse>(
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<List<SourcePreference>>(
fun getSourceSettings(sourceId: Long) = flow {
val response = client.get<List<SourcePreference>>(
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<HttpResponse>(
fun setSourceSetting(sourceId: Long, sourcePreference: SourcePreferenceChange) = flow {
val response = client.post<HttpResponse>(
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)
)

View File

@@ -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<Updates>(
fun getRecentUpdates(pageNum: Int) = flow {
val response = client.get<Updates>(
serverUrl + recentUpdatesQuery(pageNum)
)
}
emit(response)
}.flowOn(Dispatchers.IO)
suspend fun updateLibrary() = withIOContext {
client.post<HttpResponse>(
fun updateLibrary() = flow {
val response = client.post<HttpResponse>(
serverUrl + fetchUpdatesRequest()
)
}
emit(response)
}.flowOn(Dispatchers.IO)
suspend fun updateCategory(categoryId: Long) = withIOContext {
client.submitForm<HttpResponse>(
fun updateCategory(categoryId: Long) = flow {
val response = client.submitForm<HttpResponse>(
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)
}

View File

@@ -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<HttpResponse> {
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<HttpResponse> {
return chapterHandler.stopChapterDownload(chapter)
.onEach {
_downloadState.value = ChapterDownloadState.NotDownloaded
}
}
}

View File

@@ -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) {

View File

@@ -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({})
}

View File

@@ -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<String>) {

View File

@@ -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<List<Manga>>, MutableStateFlow<List<Manga>>>
@@ -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<Category>) {
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({})
}

View File

@@ -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({})
}

View File

@@ -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() {

View File

@@ -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" }
}
}
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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({})
}

View File

@@ -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)

View File

@@ -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<String>) {

View File

@@ -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<SourcePreference>.toView() = mapIndexed { index, sourcePreference ->
SourceSettingsView(index, sourcePreference)
}
private companion object : CKLogger({})
}

View File

@@ -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({})
}