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 package ca.gosyer.data.models
import ca.gosyer.data.server.interactions.ChapterInteractionHandler import ca.gosyer.data.server.interactions.ChapterInteractionHandler
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@@ -28,13 +30,15 @@ data class Chapter(
val downloaded: Boolean, val downloaded: Boolean,
val meta: ChapterMeta val meta: ChapterMeta
) { ) {
suspend fun updateRemote( fun updateRemote(
chapterHandler: ChapterInteractionHandler, chapterHandler: ChapterInteractionHandler,
pageOffset: Int = meta.juiPageOffset pageOffset: Int = meta.juiPageOffset
) { ) = flow {
if (pageOffset != meta.juiPageOffset) { 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.data.server.interactions.MangaInteractionHandler
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
@@ -37,10 +39,12 @@ data class Manga(
suspend fun updateRemote( suspend fun updateRemote(
mangaHandler: MangaInteractionHandler, mangaHandler: MangaInteractionHandler,
readerMode: String = meta.juiReaderMode readerMode: String = meta.juiReaderMode
) { ) = flow {
if (readerMode != meta.juiReaderMode) { 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 package ca.gosyer.data.server.interactions
import ca.gosyer.core.lang.withIOContext
import ca.gosyer.data.models.BackupValidationResult import ca.gosyer.data.models.BackupValidationResult
import ca.gosyer.data.server.Http import ca.gosyer.data.server.Http
import ca.gosyer.data.server.ServerPreferences 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.ContentType
import io.ktor.http.Headers import io.ktor.http.Headers
import io.ktor.http.HttpHeaders 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 me.tatarka.inject.annotations.Inject
import okio.FileSystem import okio.FileSystem
import okio.Path import okio.Path
@@ -31,8 +33,8 @@ class BackupInteractionHandler @Inject constructor(
serverPreferences: ServerPreferences serverPreferences: ServerPreferences
) : BaseInteractionHandler(client, serverPreferences) { ) : BaseInteractionHandler(client, serverPreferences) {
suspend fun importBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}) = withIOContext { fun importBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}) = flow {
client.submitFormWithBinaryData<HttpResponse>( val response = client.submitFormWithBinaryData<HttpResponse>(
serverUrl + backupFileImportRequest(), serverUrl + backupFileImportRequest(),
formData = formData { formData = formData {
append( append(
@@ -45,10 +47,11 @@ class BackupInteractionHandler @Inject constructor(
}, },
block = block block = block
) )
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun validateBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}) = withIOContext { fun validateBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}) = flow {
client.submitFormWithBinaryData<BackupValidationResult>( val response = client.submitFormWithBinaryData<BackupValidationResult>(
serverUrl + validateBackupFileRequest(), serverUrl + validateBackupFileRequest(),
formData = formData { formData = formData {
append( append(
@@ -61,12 +64,14 @@ class BackupInteractionHandler @Inject constructor(
}, },
block = block block = block
) )
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun exportBackupFile(block: HttpRequestBuilder.() -> Unit = {}) = withIOContext { fun exportBackupFile(block: HttpRequestBuilder.() -> Unit = {}) = flow {
client.get<HttpResponse>( val response = client.get<HttpResponse>(
serverUrl + backupFileExportRequest(), serverUrl + backupFileExportRequest(),
block block
) )
} emit(response)
}.flowOn(Dispatchers.IO)
} }

View File

@@ -6,7 +6,6 @@
package ca.gosyer.data.server.interactions package ca.gosyer.data.server.interactions
import ca.gosyer.core.lang.withIOContext
import ca.gosyer.data.models.Category import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Manga import ca.gosyer.data.models.Manga
import ca.gosyer.data.server.Http 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.client.statement.HttpResponse
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import io.ktor.http.Parameters 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 import me.tatarka.inject.annotations.Inject
class CategoryInteractionHandler @Inject constructor( class CategoryInteractionHandler @Inject constructor(
@@ -33,53 +35,58 @@ class CategoryInteractionHandler @Inject constructor(
serverPreferences: ServerPreferences serverPreferences: ServerPreferences
) : BaseInteractionHandler(client, serverPreferences) { ) : BaseInteractionHandler(client, serverPreferences) {
suspend fun getMangaCategories(mangaId: Long) = withIOContext { fun getMangaCategories(mangaId: Long) = flow {
client.get<List<Category>>( val response = client.get<List<Category>>(
serverUrl + getMangaCategoriesQuery(mangaId) 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 { fun addMangaToCategory(mangaId: Long, categoryId: Long) = flow {
client.get<HttpResponse>( val response = client.get<HttpResponse>(
serverUrl + addMangaToCategoryQuery(mangaId, categoryId) serverUrl + addMangaToCategoryQuery(mangaId, categoryId)
) )
} emit(response)
suspend fun addMangaToCategory(manga: Manga, category: Category) = addMangaToCategory(manga.id, category.id) }.flowOn(Dispatchers.IO)
suspend fun addMangaToCategory(manga: Manga, categoryId: Long) = addMangaToCategory(manga.id, categoryId) fun addMangaToCategory(manga: Manga, category: Category) = addMangaToCategory(manga.id, category.id)
suspend fun addMangaToCategory(mangaId: Long, category: Category) = addMangaToCategory(mangaId, 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 { fun removeMangaFromCategory(mangaId: Long, categoryId: Long) = flow {
client.delete<HttpResponse>( val response = client.delete<HttpResponse>(
serverUrl + removeMangaFromCategoryRequest(mangaId, categoryId) serverUrl + removeMangaFromCategoryRequest(mangaId, categoryId)
) )
} emit(response)
suspend fun removeMangaFromCategory(manga: Manga, category: Category) = removeMangaFromCategory(manga.id, category.id) }.flowOn(Dispatchers.IO)
suspend fun removeMangaFromCategory(manga: Manga, categoryId: Long) = removeMangaFromCategory(manga.id, categoryId) fun removeMangaFromCategory(manga: Manga, category: Category) = removeMangaFromCategory(manga.id, category.id)
suspend fun removeMangaFromCategory(mangaId: Long, category: Category) = removeMangaFromCategory(mangaId, 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 { fun getCategories(dropDefault: Boolean = false) = flow {
client.get<List<Category>>( val response = client.get<List<Category>>(
serverUrl + getCategoriesQuery() serverUrl + getCategoriesQuery()
).let { categories -> ).let { categories ->
if (dropDefault) { if (dropDefault) {
categories.filterNot { it.name.equals("default", true) } categories.filterNot { it.name.equals("default", true) }
} else categories } else categories
} }
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun createCategory(name: String) = withIOContext { fun createCategory(name: String) = flow {
client.submitForm<HttpResponse>( val response = client.submitForm<HttpResponse>(
serverUrl + createCategoryRequest(), serverUrl + createCategoryRequest(),
formParameters = Parameters.build { formParameters = Parameters.build {
append("name", name) append("name", name)
} }
) )
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun modifyCategory(categoryId: Long, name: String? = null, isLanding: Boolean? = null) = withIOContext { fun modifyCategory(categoryId: Long, name: String? = null, isLanding: Boolean? = null) = flow {
client.submitForm<HttpResponse>( val response = client.submitForm<HttpResponse>(
serverUrl + categoryModifyRequest(categoryId), serverUrl + categoryModifyRequest(categoryId),
formParameters = Parameters.build { formParameters = Parameters.build {
if (name != null) { if (name != null) {
@@ -92,11 +99,12 @@ class CategoryInteractionHandler @Inject constructor(
) { ) {
method = HttpMethod.Patch method = HttpMethod.Patch
} }
} emit(response)
suspend fun modifyCategory(category: Category, name: String? = null, isLanding: Boolean? = null) = modifyCategory(category.id, name, isLanding) }.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 { fun reorderCategory(to: Int, from: Int) = flow {
client.submitForm<HttpResponse>( val response = client.submitForm<HttpResponse>(
serverUrl + categoryReorderRequest(), serverUrl + categoryReorderRequest(),
formParameters = Parameters.build { formParameters = Parameters.build {
append("to", to.toString()) append("to", to.toString())
@@ -105,19 +113,22 @@ class CategoryInteractionHandler @Inject constructor(
) { ) {
method = HttpMethod.Patch method = HttpMethod.Patch
} }
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun deleteCategory(categoryId: Long) = withIOContext { fun deleteCategory(categoryId: Long) = flow {
client.delete<HttpResponse>( val response = client.delete<HttpResponse>(
serverUrl + categoryDeleteRequest(categoryId) serverUrl + categoryDeleteRequest(categoryId)
) )
} emit(response)
suspend fun deleteCategory(category: Category) = deleteCategory(category.id) }.flowOn(Dispatchers.IO)
fun deleteCategory(category: Category) = deleteCategory(category.id)
suspend fun getMangaFromCategory(categoryId: Long) = withIOContext { fun getMangaFromCategory(categoryId: Long) = flow {
client.get<List<Manga>>( val response = client.get<List<Manga>>(
serverUrl + getMangaInCategoryQuery(categoryId) serverUrl + getMangaInCategoryQuery(categoryId)
) )
} emit(response)
suspend fun getMangaFromCategory(category: Category) = getMangaFromCategory(category.id) }.flowOn(Dispatchers.IO)
fun getMangaFromCategory(category: Category) = getMangaFromCategory(category.id)
} }

View File

@@ -6,7 +6,6 @@
package ca.gosyer.data.server.interactions package ca.gosyer.data.server.interactions
import ca.gosyer.core.lang.withIOContext
import ca.gosyer.data.models.Chapter import ca.gosyer.data.models.Chapter
import ca.gosyer.data.models.Manga import ca.gosyer.data.models.Manga
import ca.gosyer.data.server.Http 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.HttpMethod
import io.ktor.http.Parameters import io.ktor.http.Parameters
import io.ktor.utils.io.ByteReadChannel 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 import me.tatarka.inject.annotations.Inject
class ChapterInteractionHandler @Inject constructor( class ChapterInteractionHandler @Inject constructor(
@@ -35,8 +37,8 @@ class ChapterInteractionHandler @Inject constructor(
serverPreferences: ServerPreferences serverPreferences: ServerPreferences
) : BaseInteractionHandler(client, serverPreferences) { ) : BaseInteractionHandler(client, serverPreferences) {
suspend fun getChapters(mangaId: Long, refresh: Boolean = false) = withIOContext { fun getChapters(mangaId: Long, refresh: Boolean = false) = flow {
client.get<List<Chapter>>( val response = client.get<List<Chapter>>(
serverUrl + getMangaChaptersQuery(mangaId) serverUrl + getMangaChaptersQuery(mangaId)
) { ) {
url { 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 { fun getChapter(mangaId: Long, chapterIndex: Int) = flow {
client.get<Chapter>( val response = client.get<Chapter>(
serverUrl + getChapterQuery(mangaId, chapterIndex) 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, mangaId: Long,
chapterIndex: Int, chapterIndex: Int,
read: Boolean? = null, read: Boolean? = null,
bookmarked: Boolean? = null, bookmarked: Boolean? = null,
lastPageRead: Int? = null, lastPageRead: Int? = null,
markPreviousRead: Boolean? = null markPreviousRead: Boolean? = null
) = withIOContext { ) = flow {
client.submitForm<HttpResponse>( val response = client.submitForm<HttpResponse>(
serverUrl + updateChapterRequest(mangaId, chapterIndex), serverUrl + updateChapterRequest(mangaId, chapterIndex),
formParameters = Parameters.build { formParameters = Parameters.build {
if (read != null) { if (read != null) {
@@ -88,9 +92,10 @@ class ChapterInteractionHandler @Inject constructor(
) { ) {
method = HttpMethod.Patch method = HttpMethod.Patch
} }
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun updateChapter( fun updateChapter(
manga: Manga, manga: Manga,
chapterIndex: Int, chapterIndex: Int,
read: Boolean? = null, read: Boolean? = null,
@@ -106,7 +111,7 @@ class ChapterInteractionHandler @Inject constructor(
markPreviousRead markPreviousRead
) )
suspend fun updateChapter( fun updateChapter(
manga: Manga, manga: Manga,
chapter: Chapter, chapter: Chapter,
read: Boolean? = null, read: Boolean? = null,
@@ -122,57 +127,61 @@ class ChapterInteractionHandler @Inject constructor(
markPreviousRead markPreviousRead
) )
suspend fun getPage(mangaId: Long, chapterIndex: Int, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = withIOContext { fun getPage(mangaId: Long, chapterIndex: Int, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = flow {
client.get<ByteReadChannel>( val response = client.get<ByteReadChannel>(
serverUrl + getPageQuery(mangaId, chapterIndex, pageNum), serverUrl + getPageQuery(mangaId, chapterIndex, pageNum),
block 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 { fun deleteChapterDownload(mangaId: Long, chapterIndex: Int) = flow {
client.delete<HttpResponse>( val response = client.delete<HttpResponse>(
serverUrl + deleteDownloadedChapterRequest(mangaId, chapterIndex) 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 { fun queueChapterDownload(mangaId: Long, chapterIndex: Int) = flow {
client.get<HttpResponse>( val response = client.get<HttpResponse>(
serverUrl + queueDownloadChapterRequest(mangaId, chapterIndex) 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 { fun stopChapterDownload(mangaId: Long, chapterIndex: Int) = flow {
client.delete<HttpResponse>( val response = client.delete<HttpResponse>(
serverUrl + stopDownloadingChapterRequest(mangaId, chapterIndex) 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 { fun updateChapterMeta(mangaId: Long, chapterIndex: Int, key: String, value: String) = flow {
client.submitForm<HttpResponse>( val response = client.submitForm<HttpResponse>(
serverUrl + updateChapterMetaRequest(mangaId, chapterIndex), serverUrl + updateChapterMetaRequest(mangaId, chapterIndex),
formParameters = Parameters.build { formParameters = Parameters.build {
append("key", key) append("key", key)
@@ -181,11 +190,12 @@ class ChapterInteractionHandler @Inject constructor(
) { ) {
method = HttpMethod.Patch 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 package ca.gosyer.data.server.interactions
import ca.gosyer.core.lang.withIOContext
import ca.gosyer.data.server.Http import ca.gosyer.data.server.Http
import ca.gosyer.data.server.ServerPreferences import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.requests.downloadsClearRequest 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 ca.gosyer.data.server.requests.downloadsStopRequest
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse 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 import me.tatarka.inject.annotations.Inject
class DownloadInteractionHandler @Inject constructor( class DownloadInteractionHandler @Inject constructor(
@@ -21,21 +23,24 @@ class DownloadInteractionHandler @Inject constructor(
serverPreferences: ServerPreferences serverPreferences: ServerPreferences
) : BaseInteractionHandler(client, serverPreferences) { ) : BaseInteractionHandler(client, serverPreferences) {
suspend fun startDownloading() = withIOContext { fun startDownloading() = flow {
client.get<HttpResponse>( val response = client.get<HttpResponse>(
serverUrl + downloadsStartRequest() serverUrl + downloadsStartRequest()
) )
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun stopDownloading() = withIOContext { fun stopDownloading() = flow {
client.get<HttpResponse>( val response = client.get<HttpResponse>(
serverUrl + downloadsStopRequest() serverUrl + downloadsStopRequest()
) )
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun clearDownloadQueue() = withIOContext { fun clearDownloadQueue() = flow {
client.get<HttpResponse>( val response = client.get<HttpResponse>(
serverUrl + downloadsClearRequest() serverUrl + downloadsClearRequest()
) )
} emit(response)
}.flowOn(Dispatchers.IO)
} }

View File

@@ -6,7 +6,6 @@
package ca.gosyer.data.server.interactions package ca.gosyer.data.server.interactions
import ca.gosyer.core.lang.withIOContext
import ca.gosyer.data.models.Extension import ca.gosyer.data.models.Extension
import ca.gosyer.data.server.Http import ca.gosyer.data.server.Http
import ca.gosyer.data.server.ServerPreferences 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.request.get
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import io.ktor.utils.io.ByteReadChannel 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 import me.tatarka.inject.annotations.Inject
class ExtensionInteractionHandler @Inject constructor( class ExtensionInteractionHandler @Inject constructor(
@@ -26,34 +28,39 @@ class ExtensionInteractionHandler @Inject constructor(
serverPreferences: ServerPreferences serverPreferences: ServerPreferences
) : BaseInteractionHandler(client, serverPreferences) { ) : BaseInteractionHandler(client, serverPreferences) {
suspend fun getExtensionList() = withIOContext { fun getExtensionList() = flow {
client.get<List<Extension>>( val response = client.get<List<Extension>>(
serverUrl + extensionListQuery() serverUrl + extensionListQuery()
) )
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun installExtension(extension: Extension) = withIOContext { fun installExtension(extension: Extension) = flow {
client.get<HttpResponse>( val response = client.get<HttpResponse>(
serverUrl + apkInstallQuery(extension.pkgName) serverUrl + apkInstallQuery(extension.pkgName)
) )
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun updateExtension(extension: Extension) = withIOContext { fun updateExtension(extension: Extension) = flow {
client.get<HttpResponse>( val response = client.get<HttpResponse>(
serverUrl + apkUpdateQuery(extension.pkgName) serverUrl + apkUpdateQuery(extension.pkgName)
) )
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun uninstallExtension(extension: Extension) = withIOContext { fun uninstallExtension(extension: Extension) = flow {
client.get<HttpResponse>( val response = client.get<HttpResponse>(
serverUrl + apkUninstallQuery(extension.pkgName) serverUrl + apkUninstallQuery(extension.pkgName)
) )
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun getApkIcon(extension: Extension, block: HttpRequestBuilder.() -> Unit) = withIOContext { fun getApkIcon(extension: Extension, block: HttpRequestBuilder.() -> Unit) = flow {
client.get<ByteReadChannel>( val response = client.get<ByteReadChannel>(
serverUrl + apkIconQuery(extension.apkName), serverUrl + apkIconQuery(extension.apkName),
block block
) )
} emit(response)
}.flowOn(Dispatchers.IO)
} }

View File

@@ -6,7 +6,6 @@
package ca.gosyer.data.server.interactions package ca.gosyer.data.server.interactions
import ca.gosyer.core.lang.withIOContext
import ca.gosyer.data.models.Manga import ca.gosyer.data.models.Manga
import ca.gosyer.data.server.Http import ca.gosyer.data.server.Http
import ca.gosyer.data.server.ServerPreferences 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.delete
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse 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 import me.tatarka.inject.annotations.Inject
class LibraryInteractionHandler @Inject constructor( class LibraryInteractionHandler @Inject constructor(
@@ -22,19 +24,21 @@ class LibraryInteractionHandler @Inject constructor(
serverPreferences: ServerPreferences serverPreferences: ServerPreferences
) : BaseInteractionHandler(client, serverPreferences) { ) : BaseInteractionHandler(client, serverPreferences) {
suspend fun addMangaToLibrary(mangaId: Long) = withIOContext { fun addMangaToLibrary(mangaId: Long) = flow {
client.get<HttpResponse>( val response = client.get<HttpResponse>(
serverUrl + addMangaToLibraryQuery(mangaId) 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 { fun removeMangaFromLibrary(mangaId: Long) = flow {
client.delete<HttpResponse>( val response = client.delete<HttpResponse>(
serverUrl + removeMangaFromLibraryRequest(mangaId) 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 package ca.gosyer.data.server.interactions
import ca.gosyer.core.lang.withIOContext
import ca.gosyer.data.models.Manga import ca.gosyer.data.models.Manga
import ca.gosyer.data.server.Http import ca.gosyer.data.server.Http
import ca.gosyer.data.server.ServerPreferences 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.HttpMethod
import io.ktor.http.Parameters import io.ktor.http.Parameters
import io.ktor.utils.io.ByteReadChannel 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 import me.tatarka.inject.annotations.Inject
class MangaInteractionHandler @Inject constructor( class MangaInteractionHandler @Inject constructor(
@@ -28,8 +30,8 @@ class MangaInteractionHandler @Inject constructor(
serverPreferences: ServerPreferences serverPreferences: ServerPreferences
) : BaseInteractionHandler(client, serverPreferences) { ) : BaseInteractionHandler(client, serverPreferences) {
suspend fun getManga(mangaId: Long, refresh: Boolean = false) = withIOContext { fun getManga(mangaId: Long, refresh: Boolean = false) = flow {
client.get<Manga>( val response = client.get<Manga>(
serverUrl + mangaQuery(mangaId) serverUrl + mangaQuery(mangaId)
) { ) {
url { 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 { fun getMangaThumbnail(mangaId: Long, block: HttpRequestBuilder.() -> Unit) = flow {
client.get<ByteReadChannel>( val response = client.get<ByteReadChannel>(
serverUrl + mangaThumbnailQuery(mangaId), serverUrl + mangaThumbnailQuery(mangaId),
block block
) )
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun updateMangaMeta(mangaId: Long, key: String, value: String) = withIOContext { fun updateMangaMeta(mangaId: Long, key: String, value: String) = flow {
client.submitForm<HttpResponse>( val response = client.submitForm<HttpResponse>(
serverUrl + updateMangaMetaRequest(mangaId), serverUrl + updateMangaMetaRequest(mangaId),
formParameters = Parameters.build { formParameters = Parameters.build {
append("key", key) append("key", key)
@@ -59,7 +63,8 @@ class MangaInteractionHandler @Inject constructor(
) { ) {
method = HttpMethod.Patch 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 package ca.gosyer.data.server.interactions
import ca.gosyer.core.lang.withIOContext
import ca.gosyer.data.models.MangaPage import ca.gosyer.data.models.MangaPage
import ca.gosyer.data.models.Source import ca.gosyer.data.models.Source
import ca.gosyer.data.models.sourcefilters.SourceFilter 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.client.statement.HttpResponse
import io.ktor.http.ContentType import io.ktor.http.ContentType
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.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
@@ -40,45 +42,49 @@ class SourceInteractionHandler @Inject constructor(
serverPreferences: ServerPreferences serverPreferences: ServerPreferences
) : BaseInteractionHandler(client, serverPreferences) { ) : BaseInteractionHandler(client, serverPreferences) {
suspend fun getSourceList() = withIOContext { fun getSourceList() = flow {
client.get<List<Source>>( val response = client.get<List<Source>>(
serverUrl + sourceListQuery() serverUrl + sourceListQuery()
) )
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun getSourceInfo(sourceId: Long) = withIOContext { fun getSourceInfo(sourceId: Long) = flow {
client.get<Source>( val response = client.get<Source>(
serverUrl + sourceInfoQuery(sourceId) 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 { fun getPopularManga(sourceId: Long, pageNum: Int) = flow {
client.get<MangaPage>( val response = client.get<MangaPage>(
serverUrl + sourcePopularQuery(sourceId, pageNum) 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, source.id,
pageNum pageNum
) )
suspend fun getLatestManga(sourceId: Long, pageNum: Int) = withIOContext { fun getLatestManga(sourceId: Long, pageNum: Int) = flow {
client.get<MangaPage>( val response = client.get<MangaPage>(
serverUrl + sourceLatestQuery(sourceId, pageNum) 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, source.id,
pageNum pageNum
) )
// TODO: 2021-03-14 // TODO: 2021-03-14
suspend fun getGlobalSearchResults(searchTerm: String) = withIOContext { fun getGlobalSearchResults(searchTerm: String) = flow {
client.get<HttpResponse>( val response = client.get<HttpResponse>(
serverUrl + globalSearchQuery() serverUrl + globalSearchQuery()
) { ) {
url { url {
@@ -87,10 +93,11 @@ class SourceInteractionHandler @Inject constructor(
} }
} }
} }
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun getSearchResults(sourceId: Long, searchTerm: String, pageNum: Int) = withIOContext { fun getSearchResults(sourceId: Long, searchTerm: String, pageNum: Int) = flow {
client.get<MangaPage>( val response = client.get<MangaPage>(
serverUrl + sourceSearchQuery(sourceId) serverUrl + sourceSearchQuery(sourceId)
) { ) {
url { 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, source.id,
searchTerm, searchTerm,
pageNum pageNum
) )
suspend fun getFilterList(sourceId: Long, reset: Boolean = false) = withIOContext { fun getFilterList(sourceId: Long, reset: Boolean = false) = flow {
client.get<List<SourceFilter>>( val response = client.get<List<SourceFilter>>(
serverUrl + getFilterListQuery(sourceId) serverUrl + getFilterListQuery(sourceId)
) { ) {
url { 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 { fun setFilter(sourceId: Long, sourceFilter: SourceFilterChange) = flow {
client.post<HttpResponse>( val response = client.post<HttpResponse>(
serverUrl + setFilterRequest(sourceId) serverUrl + setFilterRequest(sourceId)
) { ) {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
body = sourceFilter 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, sourceId,
SourceFilterChange(position, value) 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, sourceId,
SourceFilterChange( SourceFilterChange(
parentPosition, parentPosition,
@@ -144,24 +154,26 @@ class SourceInteractionHandler @Inject constructor(
) )
) )
suspend fun getSourceSettings(sourceId: Long) = withIOContext { fun getSourceSettings(sourceId: Long) = flow {
client.get<List<SourcePreference>>( val response = client.get<List<SourcePreference>>(
serverUrl + getSourceSettingsQuery(sourceId) 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 { fun setSourceSetting(sourceId: Long, sourcePreference: SourcePreferenceChange) = flow {
client.post<HttpResponse>( val response = client.post<HttpResponse>(
serverUrl + updateSourceSettingQuery(sourceId) serverUrl + updateSourceSettingQuery(sourceId)
) { ) {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
body = sourcePreference 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, sourceId,
SourcePreferenceChange(position, value) SourcePreferenceChange(position, value)
) )

View File

@@ -6,7 +6,6 @@
package ca.gosyer.data.server.interactions package ca.gosyer.data.server.interactions
import ca.gosyer.core.lang.withIOContext
import ca.gosyer.data.models.Category import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Updates import ca.gosyer.data.models.Updates
import ca.gosyer.data.server.Http 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.request.post
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import io.ktor.http.Parameters 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 import me.tatarka.inject.annotations.Inject
class UpdatesInteractionHandler @Inject constructor( class UpdatesInteractionHandler @Inject constructor(
@@ -25,26 +27,29 @@ class UpdatesInteractionHandler @Inject constructor(
serverPreferences: ServerPreferences serverPreferences: ServerPreferences
) : BaseInteractionHandler(client, serverPreferences) { ) : BaseInteractionHandler(client, serverPreferences) {
suspend fun getRecentUpdates(pageNum: Int) = withIOContext { fun getRecentUpdates(pageNum: Int) = flow {
client.get<Updates>( val response = client.get<Updates>(
serverUrl + recentUpdatesQuery(pageNum) serverUrl + recentUpdatesQuery(pageNum)
) )
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun updateLibrary() = withIOContext { fun updateLibrary() = flow {
client.post<HttpResponse>( val response = client.post<HttpResponse>(
serverUrl + fetchUpdatesRequest() serverUrl + fetchUpdatesRequest()
) )
} emit(response)
}.flowOn(Dispatchers.IO)
suspend fun updateCategory(categoryId: Long) = withIOContext { fun updateCategory(categoryId: Long) = flow {
client.submitForm<HttpResponse>( val response = client.submitForm<HttpResponse>(
serverUrl + fetchUpdatesRequest(), serverUrl + fetchUpdatesRequest(),
formParameters = Parameters.build { formParameters = Parameters.build {
append("category", categoryId.toString()) 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.i18n.MR
import ca.gosyer.uicore.components.DropdownIconButton import ca.gosyer.uicore.components.DropdownIconButton
import ca.gosyer.uicore.resources.stringResource 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.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.onEach
data class ChapterDownloadItem( data class ChapterDownloadItem(
val manga: Manga?, val manga: Manga?,
@@ -71,14 +74,18 @@ data class ChapterDownloadItem(
_downloadChapterFlow.value = downloadingChapter _downloadChapterFlow.value = downloadingChapter
} }
suspend fun deleteDownload(chapterHandler: ChapterInteractionHandler) { fun deleteDownload(chapterHandler: ChapterInteractionHandler): Flow<HttpResponse> {
chapterHandler.deleteChapterDownload(chapter) return chapterHandler.deleteChapterDownload(chapter)
_downloadState.value = ChapterDownloadState.NotDownloaded .onEach {
_downloadState.value = ChapterDownloadState.NotDownloaded
}
} }
suspend fun stopDownloading(chapterHandler: ChapterInteractionHandler) { fun stopDownloading(chapterHandler: ChapterInteractionHandler): Flow<HttpResponse> {
chapterHandler.stopChapterDownload(chapter) return chapterHandler.stopChapterDownload(chapter)
_downloadState.value = ChapterDownloadState.NotDownloaded .onEach {
_downloadState.value = ChapterDownloadState.NotDownloaded
}
} }
} }

View File

@@ -6,7 +6,6 @@
package ca.gosyer.ui.categories package ca.gosyer.ui.categories
import ca.gosyer.core.lang.throwIfCancellation
import ca.gosyer.core.logging.CKLogger import ca.gosyer.core.logging.CKLogger
import ca.gosyer.data.models.Category import ca.gosyer.data.models.Category
import ca.gosyer.data.server.interactions.CategoryInteractionHandler import ca.gosyer.data.server.interactions.CategoryInteractionHandler
@@ -14,7 +13,11 @@ import ca.gosyer.uicore.vm.ContextWrapper
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow 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 import me.tatarka.inject.annotations.Inject
class CategoriesScreenViewModel @Inject constructor( class CategoriesScreenViewModel @Inject constructor(
@@ -32,21 +35,22 @@ class CategoriesScreenViewModel @Inject constructor(
getCategories() getCategories()
} }
fun getCategories() { private fun getCategories() {
scope.launch { _categories.value = emptyList()
_categories.value = emptyList() _isLoading.value = true
_isLoading.value = true categoryHandler.getCategories(true)
try { .onEach {
_categories.value = categoryHandler.getCategories(true) _categories.value = it
.sortedBy { it.order } .sortedBy { it.order }
.also { originalCategories = it } .also { originalCategories = it }
.map { it.toMenuCategory() } .map { it.toMenuCategory() }
} catch (e: Exception) {
e.throwIfCancellation()
} finally {
_isLoading.value = false _isLoading.value = false
} }
} .catch {
info(it) { "Error getting categories" }
_isLoading.value = false
}
.launchIn(scope)
} }
suspend fun updateRemoteCategories(manualUpdate: Boolean = false) { suspend fun updateRemoteCategories(manualUpdate: Boolean = false) {
@@ -54,23 +58,47 @@ class CategoriesScreenViewModel @Inject constructor(
val newCategories = categories.filter { it.id == null } val newCategories = categories.filter { it.id == null }
newCategories.forEach { newCategories.forEach {
categoryHandler.createCategory(it.name) categoryHandler.createCategory(it.name)
.catch {
info(it) { "Error creating category" }
}
.collect()
} }
originalCategories.forEach { originalCategory -> originalCategories.forEach { originalCategory ->
val category = categories.find { it.id == originalCategory.id } val category = categories.find { it.id == originalCategory.id }
if (category == null) { if (category == null) {
categoryHandler.deleteCategory(originalCategory) categoryHandler.deleteCategory(originalCategory)
.catch {
info(it) { "Error deleting category $originalCategory" }
}
.collect()
} else if (category.name != originalCategory.name) { } else if (category.name != originalCategory.name) {
categoryHandler.modifyCategory(originalCategory, category.name) categoryHandler.modifyCategory(originalCategory, category.name)
.catch {
info(it) { "Error modifying category $category" }
}
.collect()
} }
} }
var updatedCategories = categoryHandler.getCategories(true) var updatedCategories = categoryHandler.getCategories(true)
.catch {
info(it) { "Error getting updated categories" }
}
.singleOrNull()
categories.forEach { category -> 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) { if (category.order != updatedCategory.order) {
debug { "${category.name}: ${updatedCategory.order} to ${category.order}" } debug { "${category.name}: ${updatedCategory.order} to ${category.order}" }
categoryHandler.reorderCategory(category.order, updatedCategory.order) categoryHandler.reorderCategory(category.order, updatedCategory.order)
.catch {
info(it) { "Error re-ordering categories" }
}
.singleOrNull()
} }
updatedCategories = categoryHandler.getCategories(true) updatedCategories = categoryHandler.getCategories(true)
.catch {
info(it) { "Error getting updated categories" }
}
.singleOrNull()
} }
if (manualUpdate) { if (manualUpdate) {

View File

@@ -6,6 +6,7 @@
package ca.gosyer.ui.downloads package ca.gosyer.ui.downloads
import ca.gosyer.core.logging.CKLogger
import ca.gosyer.data.download.DownloadService import ca.gosyer.data.download.DownloadService
import ca.gosyer.data.models.Chapter import ca.gosyer.data.models.Chapter
import ca.gosyer.data.server.interactions.ChapterInteractionHandler import ca.gosyer.data.server.interactions.ChapterInteractionHandler
@@ -14,7 +15,10 @@ import ca.gosyer.uicore.vm.ContextWrapper
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope 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 import me.tatarka.inject.annotations.Inject
class DownloadsScreenViewModel @Inject constructor( class DownloadsScreenViewModel @Inject constructor(
@@ -36,35 +40,53 @@ class DownloadsScreenViewModel @Inject constructor(
val downloadQueue get() = downloadService.downloadQueue val downloadQueue get() = downloadService.downloadQueue
fun start() { fun start() {
scope.launch { downloadsHandler.startDownloading()
downloadsHandler.startDownloading() .catch {
} info(it) { "Error starting download" }
}
.launchIn(scope)
} }
fun pause() { fun pause() {
scope.launch { downloadsHandler.stopDownloading()
downloadsHandler.stopDownloading() .catch {
} info(it) { "Error stopping download" }
}
.launchIn(scope)
} }
fun clear() { fun clear() {
scope.launch { downloadsHandler.clearDownloadQueue()
downloadsHandler.clearDownloadQueue() .catch {
} info(it) { "Error clearing download" }
}
.launchIn(scope)
} }
fun stopDownload(chapter: Chapter) { 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) { fun moveToBottom(chapter: Chapter) {
scope.launch { chapterHandler.stopChapterDownload(chapter)
chapterHandler.stopChapterDownload(chapter) .onEach {
chapterHandler.queueChapterDownload(chapter) chapterHandler.queueChapterDownload(chapter)
} .catch {
info(it) { "Error adding download" }
}
.collect()
}
.catch {
info(it) { "Error stop chapter download" }
}
.launchIn(scope)
} }
fun restartDownloader() = downloadService.init() fun restartDownloader() = downloadService.init()
private companion object : CKLogger({})
} }

View File

@@ -6,7 +6,6 @@
package ca.gosyer.ui.extensions package ca.gosyer.ui.extensions
import ca.gosyer.core.lang.throwIfCancellation
import ca.gosyer.core.logging.CKLogger import ca.gosyer.core.logging.CKLogger
import ca.gosyer.data.extension.ExtensionPreferences import ca.gosyer.data.extension.ExtensionPreferences
import ca.gosyer.data.models.Extension 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.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import java.util.Locale import java.util.Locale
@@ -55,57 +56,61 @@ class ExtensionsScreenViewModel @Inject constructor(
init { init {
scope.launch { getExtensions()
getExtensions()
}
} }
private suspend fun getExtensions() { private fun getExtensions() {
try { extensionHandler.getExtensionList()
_isLoading.value = true .onEach {
extensionList.value = extensionHandler.getExtensionList() extensionList.value = it
} catch (e: Exception) { _isLoading.value = false
e.throwIfCancellation() }
extensionList.value = emptyList() .catch {
} finally { info(it) { "Error getting extensions" }
_isLoading.value = false emit(emptyList())
} _isLoading.value = false
}
.launchIn(scope)
} }
fun install(extension: Extension) { fun install(extension: Extension) {
info { "Install clicked" } info { "Install clicked" }
scope.launch { extensionHandler.installExtension(extension)
try { .onEach {
extensionHandler.installExtension(extension) getExtensions()
} catch (e: Exception) {
e.throwIfCancellation()
} }
getExtensions() .catch {
} info(it) { "Error installing extension ${extension.apkName}" }
getExtensions()
}
.launchIn(scope)
} }
fun update(extension: Extension) { fun update(extension: Extension) {
info { "Update clicked" } info { "Update clicked" }
scope.launch { extensionHandler.updateExtension(extension)
try { .onEach {
extensionHandler.updateExtension(extension) getExtensions()
} catch (e: Exception) {
e.throwIfCancellation()
} }
getExtensions() .catch {
} info(it) { "Error updating extension ${extension.apkName}" }
getExtensions()
}
.launchIn(scope)
} }
fun uninstall(extension: Extension) { fun uninstall(extension: Extension) {
info { "Uninstall clicked" } info { "Uninstall clicked" }
scope.launch { extensionHandler.uninstallExtension(extension)
try { .onEach {
extensionHandler.uninstallExtension(extension) getExtensions()
} catch (e: Exception) {
e.throwIfCancellation()
} }
getExtensions() .catch {
} info(it) { "Error uninstalling extension ${extension.apkName}" }
getExtensions()
}
.launchIn(scope)
} }
fun setEnabledLanguages(langs: Set<String>) { fun setEnabledLanguages(langs: Set<String>) {

View File

@@ -6,8 +6,8 @@
package ca.gosyer.ui.library package ca.gosyer.ui.library
import ca.gosyer.core.lang.throwIfCancellation
import ca.gosyer.core.lang.withDefaultContext import ca.gosyer.core.lang.withDefaultContext
import ca.gosyer.core.logging.CKLogger
import ca.gosyer.data.library.LibraryPreferences import ca.gosyer.data.library.LibraryPreferences
import ca.gosyer.data.models.Category import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Manga import ca.gosyer.data.models.Manga
@@ -24,11 +24,13 @@ import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.cancellable import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
private typealias CategoryItems = Pair<MutableStateFlow<List<Manga>>, MutableStateFlow<List<Manga>>> private typealias CategoryItems = Pair<MutableStateFlow<List<Manga>>, MutableStateFlow<List<Manga>>>
@@ -101,22 +103,22 @@ class LibraryScreenViewModel @Inject constructor(
} }
private fun getLibrary() { private fun getLibrary() {
scope.launch { _isLoading.value = true
_isLoading.value = true categoryHandler.getCategories()
try { .onEach { categories ->
val categories = categoryHandler.getCategories()
if (categories.isEmpty()) { if (categories.isEmpty()) {
throw Exception("Library is empty") throw Exception("Library is empty")
} }
library.categories.value = categories.sortedBy { it.order } library.categories.value = categories.sortedBy { it.order }
updateCategories(categories) updateCategories(categories)
} catch (e: Exception) {
e.throwIfCancellation()
_error.value = e.message
} finally {
_isLoading.value = false _isLoading.value = false
} }
} .catch {
_error.value = it.message
info(it) { "Error getting categories" }
_isLoading.value = false
}
.launchIn(scope)
} }
fun setSelectedPage(page: Int) { fun setSelectedPage(page: Int) {
@@ -129,9 +131,18 @@ class LibraryScreenViewModel @Inject constructor(
private suspend fun updateCategories(categories: List<Category>) { private suspend fun updateCategories(categories: List<Category>) {
withDefaultContext { withDefaultContext {
categories.map { categories.map { category ->
async { 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() }.awaitAll()
} }
@@ -146,10 +157,14 @@ class LibraryScreenViewModel @Inject constructor(
} }
fun removeManga(mangaId: Long) { fun removeManga(mangaId: Long) {
scope.launch { libraryHandler.removeMangaFromLibrary(mangaId)
libraryHandler.removeMangaFromLibrary(mangaId) .onEach {
updateCategories(getCategoriesToUpdate(mangaId)) updateCategories(getCategoriesToUpdate(mangaId))
} }
.catch {
info(it) { "Error removing manga from library" }
}
.launchIn(scope)
} }
fun updateQuery(query: String) { fun updateQuery(query: String) {
@@ -157,19 +172,20 @@ class LibraryScreenViewModel @Inject constructor(
} }
fun updateLibrary() { fun updateLibrary() {
scope.launch { updatesHandler.updateLibrary()
updatesHandler.updateLibrary() .catch {
} info(it) { "Error updating library" }
}
.launchIn(scope)
} }
fun updateCategory(category: Category) { fun updateCategory(category: Category) {
scope.launch { updatesHandler.updateCategory(category)
updatesHandler.updateCategory(category) .catch {
} info(it) { "Error updating category" }
}
.launchIn(scope)
} }
companion object { private companion object : CKLogger({})
const val QUERY_KEY = "query"
const val SELECTED_CATEGORY_KEY = "selected_category"
}
} }

View File

@@ -6,8 +6,8 @@
package ca.gosyer.ui.manga package ca.gosyer.ui.manga
import ca.gosyer.core.lang.throwIfCancellation
import ca.gosyer.core.lang.withIOContext import ca.gosyer.core.lang.withIOContext
import ca.gosyer.core.logging.CKLogger
import ca.gosyer.data.download.DownloadService import ca.gosyer.data.download.DownloadService
import ca.gosyer.data.models.Category import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Chapter import ca.gosyer.data.models.Chapter
@@ -25,9 +25,13 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
@@ -86,9 +90,14 @@ class MangaScreenViewModel @Inject constructor(
_isLoading.value = false _isLoading.value = false
} }
scope.launch { categoryHandler.getCategories(true)
_categories.value = categoryHandler.getCategories(true) .onEach {
} _categories.value = it
}
.catch {
info(it) { "Error getting categories" }
}
.launchIn(scope)
} }
fun loadManga() { fun loadManga() {
@@ -124,22 +133,34 @@ class MangaScreenViewModel @Inject constructor(
private suspend fun refreshMangaAsync(mangaId: Long, refresh: Boolean = false) = withIOContext { private suspend fun refreshMangaAsync(mangaId: Long, refresh: Boolean = false) = withIOContext {
async { async {
try { mangaHandler.getManga(mangaId, refresh)
_manga.value = mangaHandler.getManga(mangaId, refresh) .onEach {
_mangaCategories.value = categoryHandler.getMangaCategories(mangaId) _manga.value = it
} catch (e: Exception) { }
e.throwIfCancellation() .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 { private suspend fun refreshChaptersAsync(mangaId: Long, refresh: Boolean = false) = withIOContext {
async { async {
try { _chapters.value = chapterHandler.getChapters(mangaId, refresh)
_chapters.value = chapterHandler.getChapters(mangaId, refresh).toDownloadChapters() .catch {
} catch (e: Exception) { info(it) { "Error getting chapters" }
e.throwIfCancellation() emit(emptyList())
} }
.single()
.toDownloadChapters()
} }
} }
@@ -148,6 +169,10 @@ class MangaScreenViewModel @Inject constructor(
manga.value?.let { manga -> manga.value?.let { manga ->
if (manga.inLibrary) { if (manga.inLibrary) {
libraryHandler.removeMangaFromLibrary(manga) libraryHandler.removeMangaFromLibrary(manga)
.catch {
info(it) { "Error toggling favorite" }
}
.collect()
refreshMangaAsync(manga.id).await() refreshMangaAsync(manga.id).await()
} else { } else {
if (categories.value.isEmpty()) { if (categories.value.isEmpty()) {
@@ -166,12 +191,24 @@ class MangaScreenViewModel @Inject constructor(
if (manga.inLibrary) { if (manga.inLibrary) {
oldCategories.filterNot { it in categories }.forEach { oldCategories.filterNot { it in categories }.forEach {
categoryHandler.removeMangaFromCategory(manga, it) categoryHandler.removeMangaFromCategory(manga, it)
.catch {
info(it) { "Error removing manga from category" }
}
.collect()
} }
} else { } else {
libraryHandler.addMangaToLibrary(manga) libraryHandler.addMangaToLibrary(manga)
.catch {
info(it) { "Error Adding manga to library" }
}
.collect()
} }
categories.filterNot { it in oldCategories }.forEach { categories.filterNot { it in oldCategories }.forEach {
categoryHandler.addMangaToCategory(manga, it) categoryHandler.addMangaToCategory(manga, it)
.catch {
info(it) { "Error adding manga to category" }
}
.collect()
} }
refreshMangaAsync(manga.id).await() refreshMangaAsync(manga.id).await()
} }
@@ -189,8 +226,22 @@ class MangaScreenViewModel @Inject constructor(
fun toggleRead(index: Int) { fun toggleRead(index: Int) {
scope.launch { scope.launch {
manga.value?.let { manga -> manga.value?.let { manga ->
chapterHandler.updateChapter(manga, index, read = !_chapters.value.first { it.chapter.index == index }.chapter.read) chapterHandler.updateChapter(
_chapters.value = chapterHandler.getChapters(manga).toDownloadChapters() 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) { fun toggleBookmarked(index: Int) {
scope.launch { scope.launch {
manga.value?.let { manga -> manga.value?.let { manga ->
chapterHandler.updateChapter(manga, index, bookmarked = !_chapters.value.first { it.chapter.index == index }.chapter.bookmarked) chapterHandler.updateChapter(
_chapters.value = chapterHandler.getChapters(manga).toDownloadChapters() 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 { scope.launch {
manga.value?.let { manga -> manga.value?.let { manga ->
chapterHandler.updateChapter(manga, index, markPreviousRead = true) 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) { fun downloadChapter(index: Int) {
scope.launch { manga.value?.let { manga ->
manga.value?.let { manga -> chapterHandler.queueChapterDownload(manga, index)
chapterHandler.queueChapterDownload(manga, index) .catch {
} info(it) { "Error downloading chapter" }
}
.launchIn(scope)
} }
} }
fun deleteDownload(index: Int) { fun deleteDownload(index: Int) {
scope.launch { chapters.value.find { it.chapter.index == index }
chapters.value.find { it.chapter.index == index }?.deleteDownload(chapterHandler) ?.deleteDownload(chapterHandler)
} ?.catch {
info(it) { "Error deleting download" }
}
?.launchIn(scope)
} }
fun stopDownloadingChapter(index: Int) { fun stopDownloadingChapter(index: Int) {
scope.launch { chapters.value.find { it.chapter.index == index }
chapters.value.find { it.chapter.index == index }?.stopDownloading(chapterHandler) ?.stopDownloading(chapterHandler)
} ?.catch {
info(it) { "Error stopping download" }
}
?.launchIn(scope)
} }
override fun onDispose() { override fun onDispose() {
@@ -242,4 +326,6 @@ class MangaScreenViewModel @Inject constructor(
} }
data class Params(val mangaId: Long) data class Params(val mangaId: Long)
private companion object : CKLogger({})
} }

View File

@@ -7,7 +7,6 @@
package ca.gosyer.ui.reader package ca.gosyer.ui.reader
import ca.gosyer.core.lang.launchDefault import ca.gosyer.core.lang.launchDefault
import ca.gosyer.core.lang.throwIfCancellation
import ca.gosyer.core.logging.CKLogger import ca.gosyer.core.logging.CKLogger
import ca.gosyer.core.prefs.getAsFlow import ca.gosyer.core.prefs.getAsFlow
import ca.gosyer.data.models.Chapter import ca.gosyer.data.models.Chapter
@@ -34,10 +33,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
@@ -150,10 +153,15 @@ class ReaderMenuViewModel @Inject constructor(
fun setMangaReaderMode(mode: String) { fun setMangaReaderMode(mode: String) {
scope.launchDefault { scope.launchDefault {
_manga.value?.updateRemote( _manga.value
mangaHandler, ?.updateRemote(
mode mangaHandler,
) mode
)
?.catch {
info(it) { "Error updating manga reader mode" }
}
?.collect()
initManga(params.mangaId) initManga(params.mangaId)
} }
} }
@@ -185,35 +193,37 @@ class ReaderMenuViewModel @Inject constructor(
} }
private suspend fun initManga(mangaId: Long) { private suspend fun initManga(mangaId: Long) {
try { mangaHandler.getManga(mangaId)
_manga.value = mangaHandler.getManga(mangaId) .onEach {
} catch (e: Exception) { _manga.value = it
e.throwIfCancellation() }
_state.value = ReaderChapter.State.Error(e) .catch {
throw e _state.value = ReaderChapter.State.Error(it)
} info(it) { "Error loading manga" }
}
.collect()
} }
private suspend fun initChapters(mangaId: Long, chapterIndex: Int) { private suspend fun initChapters(mangaId: Long, chapterIndex: Int) {
resetValues() resetValues()
val chapter = ReaderChapter( val chapter = ReaderChapter(
try { chapterHandler.getChapter(mangaId, chapterIndex)
chapterHandler.getChapter(mangaId, chapterIndex) .catch {
} catch (e: Exception) { _state.value = ReaderChapter.State.Error(it)
e.throwIfCancellation() info(it) { "Error getting chapter" }
_state.value = ReaderChapter.State.Error(e) }
throw e .singleOrNull() ?: return
}
) )
val pages = loader.loadChapter(chapter) val pages = loader.loadChapter(chapter)
viewerChapters.currChapter.value = chapter viewerChapters.currChapter.value = chapter
scope.launchDefault { scope.launchDefault {
val chapters = try { val chapters = chapterHandler.getChapters(mangaId)
chapterHandler.getChapters(mangaId) .catch {
} catch (e: Exception) { info(it) { "Error getting chapter list" }
e.throwIfCancellation() emit(emptyList())
emptyList() }
} .single()
val nextChapter = chapters.find { it.index == chapterIndex + 1 } val nextChapter = chapters.find { it.index == chapterIndex + 1 }
if (nextChapter != null) { if (nextChapter != null) {
viewerChapters.nextChapter.value = ReaderChapter( viewerChapters.nextChapter.value = ReaderChapter(
@@ -259,17 +269,23 @@ class ReaderMenuViewModel @Inject constructor(
.launchIn(chapter.scope) .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) chapterHandler.updateChapter(mangaId, chapter.chapter.index, true)
.catch {
info(it) { "Error marking chapter read" }
}
.launchIn(scope)
} }
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
fun sendProgress(chapter: Chapter? = this.chapter.value?.chapter, lastPageRead: Int = currentPage.value) { fun sendProgress(chapter: Chapter? = this.chapter.value?.chapter, lastPageRead: Int = currentPage.value) {
chapter ?: return chapter ?: return
if (chapter.read) 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) { fun updateLastPageReadOffset(offset: Int) {
@@ -278,9 +294,11 @@ class ReaderMenuViewModel @Inject constructor(
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
private fun updateLastPageReadOffset(chapter: Chapter, offset: Int) { 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() { override fun onDispose() {

View File

@@ -22,6 +22,10 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow 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 kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@@ -58,24 +62,30 @@ class TachideskPageLoader(
val page = priorityPage.page val page = priorityPage.page
debug { "Loading page ${page.index}" } debug { "Loading page ${page.index}" }
if (page.status.value == ReaderPage.Status.QUEUE) { if (page.status.value == ReaderPage.Status.QUEUE) {
try { chapterHandler
page.bitmap.value = chapterHandler.getPage(chapter.chapter, page.index) { .getPage(chapter.chapter, page.index) {
onDownload { bytesSentTotal, contentLength -> onDownload { bytesSentTotal, contentLength ->
page.progress.value = (bytesSentTotal.toFloat() / contentLength).coerceAtMost(1.0F) page.progress.value = (bytesSentTotal.toFloat() / contentLength).coerceAtMost(1.0F)
} }
}.toImageBitmap() }
page.status.value = ReaderPage.Status.READY .onEach {
page.error.value = null page.bitmap.value = it.toImageBitmap()
} catch (e: Exception) { page.status.value = ReaderPage.Status.READY
e.throwIfCancellation() page.error.value = null
page.bitmap.value = null }
page.status.value = ReaderPage.Status.ERROR .catch {
page.error.value = e.message 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) { } catch (e: Exception) {
e.throwIfCancellation() 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.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow 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.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@@ -125,23 +129,33 @@ class SettingsBackupViewModel @Inject constructor(
val createFlow = _createFlow.asSharedFlow() val createFlow = _createFlow.asSharedFlow()
fun restoreFile(source: Source) { fun restoreFile(source: Source) {
scope.launch { scope.launch {
try { val file = try {
FileSystem.SYSTEM_TEMPORARY_DIRECTORY FileSystem.SYSTEM_TEMPORARY_DIRECTORY
.resolve("tachidesk.${Random.nextLong()}.proto.gz") .resolve("tachidesk.${Random.nextLong()}.proto.gz")
.also { file -> .also { file ->
source.saveTo(file) source.saveTo(file)
val (missingSources) = backupHandler.validateBackupFile(file)
if (missingSources.isEmpty()) {
restoreBackup(file)
} else {
_missingSourceFlow.emit(file to missingSources)
}
} }
} catch (e: Exception) { } catch (e: Exception) {
info(e) { "Error importing backup" } info(e) { "Error creating backup file" }
_restoreStatus.value = Status.Error _restoreStatus.value = Status.Error
e.throwIfCancellation() 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 _restoreStatus.value = Status.Nothing
_restoringProgress.value = null _restoringProgress.value = null
_restoring.value = true _restoring.value = true
try { backupHandler.importBackupFile(file) {
backupHandler.importBackupFile(file) { onUpload { bytesSentTotal, contentLength ->
onUpload { bytesSentTotal, contentLength -> _restoringProgress.value = (bytesSentTotal.toFloat() / contentLength).coerceAtMost(1.0F)
_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() private val mutex = Mutex()
fun exportBackup() { fun exportBackup() {
scope.launch { _creatingStatus.value = Status.Nothing
_creatingStatus.value = Status.Nothing _creatingProgress.value = null
_creatingProgress.value = null _creating.value = true
_creating.value = true backupHandler
val backup = try { .exportBackupFile {
backupHandler.exportBackupFile { onDownload { bytesSentTotal, contentLength ->
onDownload { bytesSentTotal, contentLength -> _creatingProgress.value = (bytesSentTotal.toFloat() / contentLength).coerceAtMost(0.99F)
_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 .onEach { backup ->
if (backup != null && backup.status.isSuccess()) { if (backup.status.isSuccess()) {
val filename = backup.headers["content-disposition"]?.substringAfter("filename=")?.trim('"') ?: "backup" val filename =
tempFile.value = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(filename).also { backup.headers["content-disposition"]?.substringAfter("filename=")
launch { ?.trim('"') ?: "backup"
try { tempFile.value = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(filename).also {
backup.content.toInputStream() mutex.tryLock()
.source() scope.launch {
.saveTo(it) try {
} catch (e: Exception) { backup.content.toInputStream()
e.throwIfCancellation() .source()
error(e) { "Error creating backup" } .saveTo(it)
_creatingStatus.value = Status.Error } catch (e: Exception) {
_creating.value = false e.throwIfCancellation()
} finally { info(e) { "Error creating backup" }
mutex.unlock() _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) { fun exportBackupFileFound(backupSink: Sink) {

View File

@@ -18,6 +18,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.core.logging.CKLogger
import ca.gosyer.data.library.LibraryPreferences import ca.gosyer.data.library.LibraryPreferences
import ca.gosyer.data.server.interactions.CategoryInteractionHandler import ca.gosyer.data.server.interactions.CategoryInteractionHandler
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
@@ -39,7 +40,9 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow 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 import me.tatarka.inject.annotations.Inject
class SettingsLibraryScreen : Screen { class SettingsLibraryScreen : Screen {
@@ -72,10 +75,17 @@ class SettingsLibraryViewModel @Inject constructor(
} }
fun refreshCategoryCount() { fun refreshCategoryCount() {
scope.launch { categoryHandler.getCategories(true)
_categories.value = categoryHandler.getCategories(true).size .onEach {
} _categories.value = it.size
}
.catch {
info(it) { "Error getting categories" }
}
.launchIn(scope)
} }
private companion object : CKLogger({})
} }
@Composable @Composable

View File

@@ -6,7 +6,7 @@
package ca.gosyer.ui.sources.browse 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.Manga
import ca.gosyer.data.models.MangaPage import ca.gosyer.data.models.MangaPage
import ca.gosyer.data.models.Source import ca.gosyer.data.models.Source
@@ -15,7 +15,10 @@ import ca.gosyer.uicore.vm.ContextWrapper
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class SourceScreenViewModel( class SourceScreenViewModel(
@@ -59,36 +62,33 @@ class SourceScreenViewModel(
private val _pageNum = MutableStateFlow(1) private val _pageNum = MutableStateFlow(1)
val pageNum = _pageNum.asStateFlow() val pageNum = _pageNum.asStateFlow()
private val sourceMutex = Mutex()
init { init {
scope.launch { scope.launch {
try { getPage()?.let { (mangas, hasNextPage) ->
val (mangas, hasNextPage) = getPage()
_mangas.value = mangas _mangas.value = mangas
_hasNextPage.value = hasNextPage _hasNextPage.value = hasNextPage
} catch (e: Exception) {
e.throwIfCancellation()
} finally {
_loading.value = false
} }
_loading.value = false
} }
} }
fun loadNextPage() { fun loadNextPage() {
scope.launch { scope.launch {
val hasNextPage = hasNextPage.value if (hasNextPage.value && sourceMutex.tryLock()) {
val pageNum = pageNum.value
try {
_hasNextPage.value = false
_pageNum.value++ _pageNum.value++
val page = getPage() val page = getPage()
_mangas.value += page.mangaList if (page != null) {
_hasNextPage.value = page.hasNextPage _mangas.value += page.mangaList
} catch (e: Exception) { _hasNextPage.value = page.hasNextPage
_hasNextPage.value = hasNextPage } else {
_pageNum.value = pageNum _pageNum.value--
} finally { }
_loading.value = false sourceMutex.unlock()
} }
_loading.value = false
} }
} }
@@ -104,12 +104,16 @@ class SourceScreenViewModel(
} }
} }
private suspend fun getPage(): MangaPage { private suspend fun getPage(): MangaPage? {
return when { return when {
isLatest.value -> sourceHandler.getLatestManga(source, pageNum.value) isLatest.value -> sourceHandler.getLatestManga(source, pageNum.value)
_query.value != null || _usingFilters.value -> sourceHandler.getSearchResults(source, _query.value.orEmpty(), pageNum.value) _query.value != null || _usingFilters.value -> sourceHandler.getSearchResults(source, _query.value.orEmpty(), pageNum.value)
else -> sourceHandler.getPopularManga(source, pageNum.value) else -> sourceHandler.getPopularManga(source, pageNum.value)
} }
.catch {
info(it) { "Error getting source page" }
}
.singleOrNull()
} }
fun startSearch(query: String?) { fun startSearch(query: String?) {
@@ -136,4 +140,6 @@ class SourceScreenViewModel(
} }
data class Params(val source: Source) data class Params(val source: Source)
private companion object : CKLogger({})
} }

View File

@@ -6,7 +6,6 @@
package ca.gosyer.ui.sources.browse.filter package ca.gosyer.ui.sources.browse.filter
import ca.gosyer.core.lang.throwIfCancellation
import ca.gosyer.core.logging.CKLogger import ca.gosyer.core.logging.CKLogger
import ca.gosyer.data.models.sourcefilters.SourceFilter import ca.gosyer.data.models.sourcefilters.SourceFilter
import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.data.server.interactions.SourceInteractionHandler
@@ -15,12 +14,13 @@ import ca.gosyer.uicore.vm.ContextWrapper
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
@@ -69,6 +69,7 @@ class SourceFiltersViewModel(
childFilter.index, childFilter.index,
it it
) )
.collect()
getFilters() getFilters()
} }
.launchIn(this) .launchIn(this)
@@ -77,13 +78,18 @@ class SourceFiltersViewModel(
filter.state.drop(1).filterNotNull() filter.state.drop(1).filterNotNull()
.onEach { .onEach {
sourceHandler.setFilter(sourceId, filter.index, it) sourceHandler.setFilter(sourceId, filter.index, it)
.collect()
getFilters() getFilters()
} }
.launchIn(this) .launchIn(this)
} }
} }
} }
}.launchIn(scope) }
.catch {
info(it) { "Error with filters" }
}
.launchIn(scope)
} }
fun showingFilters(show: Boolean) { fun showingFilters(show: Boolean) {
@@ -91,21 +97,20 @@ class SourceFiltersViewModel(
} }
private fun getFilters(initialLoad: Boolean = false) { private fun getFilters(initialLoad: Boolean = false) {
scope.launch { sourceHandler.getFilterList(sourceId, reset = initialLoad)
try { .onEach {
_filters.value = sourceHandler.getFilterList(sourceId, reset = initialLoad).toView() _filters.value = it.toView()
} catch (e: Exception) {
e.throwIfCancellation()
} finally {
_loading.value = false _loading.value = false
} }
} .catch {
info(it) { "Error getting filters" }
_loading.value = false
}
.launchIn(scope)
} }
fun resetFilters() { fun resetFilters() {
scope.launch { getFilters(initialLoad = true)
getFilters(initialLoad = true)
}
} }
data class Params(val sourceId: Long) data class Params(val sourceId: Long)

View File

@@ -6,7 +6,6 @@
package ca.gosyer.ui.sources.home package ca.gosyer.ui.sources.home
import ca.gosyer.core.lang.throwIfCancellation
import ca.gosyer.core.logging.CKLogger import ca.gosyer.core.logging.CKLogger
import ca.gosyer.data.catalog.CatalogPreferences import ca.gosyer.data.catalog.CatalogPreferences
import ca.gosyer.data.models.Source 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.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class SourceHomeScreenViewModel @Inject constructor( class SourceHomeScreenViewModel @Inject constructor(
@@ -51,15 +52,16 @@ class SourceHomeScreenViewModel @Inject constructor(
} }
private fun getSources() { private fun getSources() {
scope.launch { sourceHandler.getSourceList()
try { .onEach {
installedSources.value = sourceHandler.getSourceList() installedSources.value = it
} catch (e: Exception) {
e.throwIfCancellation()
} finally {
_isLoading.value = false _isLoading.value = false
} }
} .catch {
info(it) { "Error getting sources" }
_isLoading.value = false
}
.launchIn(scope)
} }
fun setEnabledLanguages(langs: Set<String>) { fun setEnabledLanguages(langs: Set<String>) {

View File

@@ -6,7 +6,7 @@
package ca.gosyer.ui.sources.settings 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.models.sourcepreference.SourcePreference
import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.data.server.interactions.SourceInteractionHandler
import ca.gosyer.ui.sources.settings.model.SourceSettingsView 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 ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
@@ -43,6 +44,10 @@ class SourceSettingsScreenViewModel @Inject constructor(
.filterNotNull() .filterNotNull()
.onEach { .onEach {
sourceHandler.setSourceSetting(params.sourceId, setting.index, it) sourceHandler.setSourceSetting(params.sourceId, setting.index, it)
.catch {
info(it) { "Error setting source setting" }
}
.collect()
getSourceSettings() getSourceSettings()
} }
.launchIn(this) .launchIn(this)
@@ -53,15 +58,16 @@ class SourceSettingsScreenViewModel @Inject constructor(
} }
private fun getSourceSettings() { private fun getSourceSettings() {
scope.launch { sourceHandler.getSourceSettings(params.sourceId)
try { .onEach {
_sourceSettings.value = sourceHandler.getSourceSettings(params.sourceId).toView() _sourceSettings.value = it.toView()
} catch (e: Exception) {
e.throwIfCancellation()
} finally {
_loading.value = false _loading.value = false
} }
} .catch {
info(it) { "Error setting source setting" }
_loading.value = false
}
.launchIn(scope)
} }
data class Params(val sourceId: Long) data class Params(val sourceId: Long)
@@ -69,4 +75,6 @@ class SourceSettingsScreenViewModel @Inject constructor(
private fun List<SourcePreference>.toView() = mapIndexed { index, sourcePreference -> private fun List<SourcePreference>.toView() = mapIndexed { index, sourcePreference ->
SourceSettingsView(index, sourcePreference) SourceSettingsView(index, sourcePreference)
} }
private companion object : CKLogger({})
} }

View File

@@ -6,7 +6,7 @@
package ca.gosyer.ui.updates 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.download.DownloadService
import ca.gosyer.data.models.Chapter import ca.gosyer.data.models.Chapter
import ca.gosyer.data.server.interactions.ChapterInteractionHandler import ca.gosyer.data.server.interactions.ChapterInteractionHandler
@@ -16,6 +16,8 @@ import ca.gosyer.uicore.vm.ContextWrapper
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@@ -45,77 +47,89 @@ class UpdatesScreenViewModel @Inject constructor(
init { init {
scope.launch { scope.launch {
try { getUpdates()
getUpdates(1)
} catch (e: Exception) {
e.throwIfCancellation()
} finally {
_isLoading.value = false
}
} }
} }
fun loadNextPage() { fun loadNextPage() {
scope.launch { scope.launch {
if (hasNextPage.value && updatesMutex.tryLock()) { if (hasNextPage.value && updatesMutex.tryLock()) {
try { getUpdates()
getUpdates(currentPage.value++)
} catch (e: Exception) {
e.throwIfCancellation()
currentPage.value--
}
updatesMutex.unlock() updatesMutex.unlock()
} }
} }
} }
private suspend fun getUpdates(pageNum: Int) { private suspend fun getUpdates() {
val updates = updatesHandler.getRecentUpdates(pageNum) updatesHandler.getRecentUpdates(currentPage.value)
mangaIds = updates.page.map { it.manga.id }.toSet() .onEach { updates ->
mangaIds = updates.page.map { it.manga.id }.toSet()
_updates.value += updates.page.map { _updates.value += updates.page.map {
ChapterDownloadItem( ChapterDownloadItem(
it.manga, it.manga,
it.chapter it.chapter
) )
} }
downloadService.registerWatches(mangaIds).merge() downloadService.registerWatches(mangaIds).merge()
.onEach { (mangaId, chapters) -> .onEach { (mangaId, chapters) ->
_updates.value.filter { it.chapter.mangaId == mangaId } _updates.value.filter { it.chapter.mangaId == mangaId }
.forEach { .forEach {
it.updateFrom(chapters) 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) { fun downloadChapter(chapter: Chapter) {
scope.launch { chapterHandler.queueChapterDownload(chapter)
chapterHandler.queueChapterDownload(chapter) .catch {
} info(it) { "Error queueing chapter" }
}
.launchIn(scope)
} }
fun deleteDownloadedChapter(chapter: Chapter) { fun deleteDownloadedChapter(chapter: Chapter) {
scope.launch { updates.value
updates.value.find { .find {
it.chapter.mangaId == chapter.mangaId && it.chapter.mangaId == chapter.mangaId &&
it.chapter.index == chapter.index it.chapter.index == chapter.index
}?.deleteDownload(chapterHandler) }
} ?.deleteDownload(chapterHandler)
?.catch {
info(it) { "Error deleting download" }
}
?.launchIn(scope)
} }
fun stopDownloadingChapter(chapter: Chapter) { fun stopDownloadingChapter(chapter: Chapter) {
scope.launch { updates.value
updates.value.find { .find {
it.chapter.mangaId == chapter.mangaId && it.chapter.mangaId == chapter.mangaId &&
it.chapter.index == chapter.index it.chapter.index == chapter.index
}?.stopDownloading(chapterHandler) }
} ?.stopDownloading(chapterHandler)
?.catch {
info(it) { "Error stopping download" }
}
?.launchIn(scope)
} }
override fun onDispose() { override fun onDispose() {
downloadService.removeWatches(mangaIds) downloadService.removeWatches(mangaIds)
} }
private companion object : CKLogger({})
} }