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