Update Ktor to v2.0.0

This commit is contained in:
Syer10
2022-04-14 18:54:43 -04:00
parent bd05af47aa
commit 527b4a4f81
37 changed files with 278 additions and 232 deletions

View File

@@ -57,7 +57,8 @@ dependencies {
// Http client
implementation(libs.ktor.core)
implementation(libs.ktor.okHttp)
implementation(libs.ktor.serialization)
implementation(libs.ktor.contentNegotiation)
implementation(libs.ktor.serialization.json)
implementation(libs.ktor.logging)
implementation(libs.ktor.websockets)
implementation(libs.ktor.auth)

View File

@@ -30,9 +30,9 @@ import ca.gosyer.jui.data.server.requests.downloadsQuery
import ca.gosyer.jui.i18n.MR
import dev.icerock.moko.resources.desc.desc
import dev.icerock.moko.resources.format
import io.ktor.client.features.websocket.ws
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readText
import io.ktor.client.plugins.websocket.ws
import io.ktor.websocket.Frame
import io.ktor.websocket.readText
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers

View File

@@ -39,7 +39,8 @@ kotlin {
api(libs.serialization.json)
api(libs.kotlinInject.runtime)
api(libs.ktor.core)
api(libs.ktor.serialization)
api(libs.ktor.contentNegotiation)
api(libs.ktor.serialization.json)
api(libs.okio)
api(libs.logging.kmlogging)
api(libs.multiplatformSettings.core)

View File

@@ -0,0 +1,21 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.core.io
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.discardRemaining
import io.ktor.http.HttpStatusCode
import io.ktor.http.isSuccess
suspend fun HttpResponse.asSuccess() : HttpResponse = apply {
if (!status.isSuccess()) {
discardRemaining()
throw HttpException(status)
}
}
class HttpException(val status: HttpStatusCode) : IllegalStateException("HTTP error $status")

View File

@@ -38,7 +38,8 @@ kotlin {
api(libs.serialization.json)
api(libs.kotlinInject.runtime)
api(libs.ktor.core)
api(libs.ktor.serialization)
api(libs.ktor.contentNegotiation)
api(libs.ktor.serialization.json)
api(libs.ktor.auth)
api(libs.ktor.logging)
api(libs.ktor.websockets)

View File

@@ -10,8 +10,8 @@ import ca.gosyer.jui.core.lang.throwIfCancellation
import ca.gosyer.jui.data.build.BuildKonfig
import ca.gosyer.jui.data.server.Http
import ca.gosyer.jui.data.server.ServerPreferences
import io.ktor.client.features.websocket.ws
import io.ktor.http.cio.websocket.Frame
import io.ktor.client.plugins.websocket.ws
import io.ktor.websocket.Frame
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope

View File

@@ -13,15 +13,13 @@ import ca.gosyer.jui.data.download.model.DownloaderStatus
import ca.gosyer.jui.data.server.Http
import ca.gosyer.jui.data.server.ServerPreferences
import ca.gosyer.jui.data.server.requests.downloadsQuery
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readText
import kotlinx.coroutines.DelicateCoroutinesApi
import io.ktor.websocket.Frame
import io.ktor.websocket.readText
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.decodeFromString
import me.tatarka.inject.annotations.Inject
@OptIn(DelicateCoroutinesApi::class)
class DownloadService @Inject constructor(
serverPreferences: ServerPreferences,
client: Http

View File

@@ -11,15 +11,13 @@ import ca.gosyer.jui.data.library.model.UpdateStatus
import ca.gosyer.jui.data.server.Http
import ca.gosyer.jui.data.server.ServerPreferences
import ca.gosyer.jui.data.server.requests.updatesQuery
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readText
import kotlinx.coroutines.DelicateCoroutinesApi
import io.ktor.websocket.Frame
import io.ktor.websocket.readText
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.decodeFromString
import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging
@OptIn(DelicateCoroutinesApi::class)
class LibraryUpdateService @Inject constructor(
serverPreferences: ServerPreferences,
client: Http

View File

@@ -11,19 +11,21 @@ import ca.gosyer.jui.data.server.model.Auth
import ca.gosyer.jui.data.server.model.Proxy
import io.ktor.client.HttpClient
import io.ktor.client.engine.ProxyBuilder
import io.ktor.client.features.auth.providers.BasicAuthCredentials
import io.ktor.client.features.auth.providers.DigestAuthCredentials
import io.ktor.client.features.auth.providers.basic
import io.ktor.client.features.auth.providers.digest
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.KotlinxSerializer
import io.ktor.client.features.logging.LogLevel
import io.ktor.client.features.logging.Logging
import io.ktor.client.features.websocket.WebSockets
import io.ktor.client.plugins.auth.providers.BasicAuthCredentials
import io.ktor.client.plugins.auth.providers.DigestAuthCredentials
import io.ktor.client.plugins.auth.providers.basic
import io.ktor.client.plugins.auth.providers.digest
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.http.URLBuilder
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import me.tatarka.inject.annotations.Inject
import io.ktor.client.features.auth.Auth as AuthFeature
import org.lighthousegames.logging.logging
import io.ktor.client.plugins.auth.Auth as AuthPlugin
typealias Http = HttpClient
@@ -47,7 +49,7 @@ class HttpProvider @Inject constructor() {
}
when (serverPreferences.auth().get()) {
Auth.NONE -> Unit
Auth.BASIC -> install(AuthFeature) {
Auth.BASIC -> AuthPlugin {
basic {
credentials {
BasicAuthCredentials(
@@ -57,7 +59,7 @@ class HttpProvider @Inject constructor() {
}
}
}
Auth.DIGEST -> install(AuthFeature) {
Auth.DIGEST -> AuthPlugin {
digest {
credentials {
DigestAuthCredentials(
@@ -68,8 +70,8 @@ class HttpProvider @Inject constructor() {
}
}
}
install(JsonFeature) {
serializer = KotlinxSerializer(
install(ContentNegotiation) {
json(
Json {
isLenient = false
ignoreUnknownKeys = true
@@ -85,6 +87,12 @@ class HttpProvider @Inject constructor() {
} else {
LogLevel.INFO
}
logger = object : Logger {
val log = logging("HttpClient")
override fun log(message: String) {
log.info { message }
}
}
}
}
}

View File

@@ -10,6 +10,7 @@ import ca.gosyer.jui.core.prefs.Preference
import ca.gosyer.jui.core.prefs.getAsFlow
import io.ktor.http.URLBuilder
import io.ktor.http.Url
import io.ktor.http.path
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted

View File

@@ -7,6 +7,7 @@
package ca.gosyer.jui.data.server.interactions
import ca.gosyer.jui.core.io.SYSTEM
import ca.gosyer.jui.core.io.asSuccess
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.models.BackupValidationResult
import ca.gosyer.jui.data.server.Http
@@ -14,11 +15,11 @@ import ca.gosyer.jui.data.server.ServerPreferences
import ca.gosyer.jui.data.server.requests.backupFileExportRequest
import ca.gosyer.jui.data.server.requests.backupFileImportRequest
import ca.gosyer.jui.data.server.requests.validateBackupFileRequest
import io.ktor.client.call.body
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitFormWithBinaryData
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.http.ContentType
import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
@@ -36,7 +37,7 @@ class BackupInteractionHandler @Inject constructor(
) : BaseInteractionHandler(client, serverPreferences) {
fun importBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}) = flow {
val response = client.submitFormWithBinaryData<HttpResponse>(
val response = client.submitFormWithBinaryData(
serverUrl + backupFileImportRequest(),
formData = formData {
append(
@@ -48,12 +49,12 @@ class BackupInteractionHandler @Inject constructor(
)
},
block = block
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
fun validateBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}) = flow {
val response = client.submitFormWithBinaryData<BackupValidationResult>(
val response = client.submitFormWithBinaryData(
serverUrl + validateBackupFileRequest(),
formData = formData {
append(
@@ -65,15 +66,15 @@ class BackupInteractionHandler @Inject constructor(
)
},
block = block
)
).asSuccess().body<BackupValidationResult>()
emit(response)
}.flowOn(Dispatchers.IO)
fun exportBackupFile(block: HttpRequestBuilder.() -> Unit = {}) = flow {
val response = client.get<HttpResponse>(
val response = client.get(
serverUrl + backupFileExportRequest(),
block
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -6,6 +6,7 @@
package ca.gosyer.jui.data.server.interactions
import ca.gosyer.jui.core.io.asSuccess
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.models.Category
import ca.gosyer.jui.data.models.Manga
@@ -20,10 +21,10 @@ import ca.gosyer.jui.data.server.requests.getCategoriesQuery
import ca.gosyer.jui.data.server.requests.getMangaCategoriesQuery
import ca.gosyer.jui.data.server.requests.getMangaInCategoryQuery
import ca.gosyer.jui.data.server.requests.removeMangaFromCategoryRequest
import io.ktor.client.call.body
import io.ktor.client.request.delete
import io.ktor.client.request.forms.submitForm
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
@@ -37,18 +38,18 @@ class CategoryInteractionHandler @Inject constructor(
) : BaseInteractionHandler(client, serverPreferences) {
fun getMangaCategories(mangaId: Long) = flow {
val response = client.get<List<Category>>(
val response = client.get(
serverUrl + getMangaCategoriesQuery(mangaId)
)
).asSuccess().body<List<Category>>()
emit(response)
}.flowOn(Dispatchers.IO)
fun getMangaCategories(manga: Manga) = getMangaCategories(manga.id)
fun addMangaToCategory(mangaId: Long, categoryId: Long) = flow {
val response = client.get<HttpResponse>(
val response = client.get(
serverUrl + addMangaToCategoryQuery(mangaId, categoryId)
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
fun addMangaToCategory(manga: Manga, category: Category) = addMangaToCategory(manga.id, category.id)
@@ -56,9 +57,9 @@ class CategoryInteractionHandler @Inject constructor(
fun addMangaToCategory(mangaId: Long, category: Category) = addMangaToCategory(mangaId, category.id)
fun removeMangaFromCategory(mangaId: Long, categoryId: Long) = flow {
val response = client.delete<HttpResponse>(
val response = client.delete(
serverUrl + removeMangaFromCategoryRequest(mangaId, categoryId)
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
fun removeMangaFromCategory(manga: Manga, category: Category) = removeMangaFromCategory(manga.id, category.id)
@@ -66,9 +67,9 @@ class CategoryInteractionHandler @Inject constructor(
fun removeMangaFromCategory(mangaId: Long, category: Category) = removeMangaFromCategory(mangaId, category.id)
fun getCategories(dropDefault: Boolean = false) = flow {
val response = client.get<List<Category>>(
val response = client.get(
serverUrl + getCategoriesQuery()
).let { categories ->
).asSuccess().body<List<Category>>().let { categories ->
if (dropDefault) {
categories.filterNot { it.name.equals("default", true) }
} else categories
@@ -77,17 +78,17 @@ class CategoryInteractionHandler @Inject constructor(
}.flowOn(Dispatchers.IO)
fun createCategory(name: String) = flow {
val response = client.submitForm<HttpResponse>(
val response = client.submitForm(
serverUrl + createCategoryRequest(),
formParameters = Parameters.build {
append("name", name)
}
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
fun modifyCategory(categoryId: Long, name: String? = null, isLanding: Boolean? = null) = flow {
val response = client.submitForm<HttpResponse>(
val response = client.submitForm(
serverUrl + categoryModifyRequest(categoryId),
formParameters = Parameters.build {
if (name != null) {
@@ -99,13 +100,13 @@ class CategoryInteractionHandler @Inject constructor(
}
) {
method = HttpMethod.Patch
}
}.asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
fun modifyCategory(category: Category, name: String? = null, isLanding: Boolean? = null) = modifyCategory(category.id, name, isLanding)
fun reorderCategory(to: Int, from: Int) = flow {
val response = client.submitForm<HttpResponse>(
val response = client.submitForm(
serverUrl + categoryReorderRequest(),
formParameters = Parameters.build {
append("to", to.toString())
@@ -113,22 +114,22 @@ class CategoryInteractionHandler @Inject constructor(
}
) {
method = HttpMethod.Patch
}
}.asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
fun deleteCategory(categoryId: Long) = flow {
val response = client.delete<HttpResponse>(
val response = client.delete(
serverUrl + categoryDeleteRequest(categoryId)
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
fun deleteCategory(category: Category) = deleteCategory(category.id)
fun getMangaFromCategory(categoryId: Long) = flow {
val response = client.get<List<Manga>>(
val response = client.get(
serverUrl + getMangaInCategoryQuery(categoryId)
)
).asSuccess().body<List<Manga>>()
emit(response)
}.flowOn(Dispatchers.IO)
fun getMangaFromCategory(category: Category) = getMangaFromCategory(category.id)

View File

@@ -6,6 +6,7 @@
package ca.gosyer.jui.data.server.interactions
import ca.gosyer.jui.core.io.asSuccess
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.models.Chapter
import ca.gosyer.jui.data.models.Manga
@@ -19,15 +20,15 @@ import ca.gosyer.jui.data.server.requests.queueDownloadChapterRequest
import ca.gosyer.jui.data.server.requests.stopDownloadingChapterRequest
import ca.gosyer.jui.data.server.requests.updateChapterMetaRequest
import ca.gosyer.jui.data.server.requests.updateChapterRequest
import io.ktor.client.call.body
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.delete
import io.ktor.client.request.forms.submitForm
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsChannel
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
@@ -39,7 +40,7 @@ class ChapterInteractionHandler @Inject constructor(
) : BaseInteractionHandler(client, serverPreferences) {
fun getChapters(mangaId: Long, refresh: Boolean = false) = flow {
val response = client.get<List<Chapter>>(
val response = client.get(
serverUrl + getMangaChaptersQuery(mangaId)
) {
url {
@@ -47,16 +48,16 @@ class ChapterInteractionHandler @Inject constructor(
parameter("onlineFetch", true)
}
}
}
}.asSuccess().body<List<Chapter>>()
emit(response)
}.flowOn(Dispatchers.IO)
fun getChapters(manga: Manga, refresh: Boolean = false) = getChapters(manga.id, refresh)
fun getChapter(mangaId: Long, chapterIndex: Int) = flow {
val response = client.get<Chapter>(
val response = client.get(
serverUrl + getChapterQuery(mangaId, chapterIndex)
)
).asSuccess().body<Chapter>()
emit(response)
}.flowOn(Dispatchers.IO)
@@ -74,7 +75,7 @@ class ChapterInteractionHandler @Inject constructor(
lastPageRead: Int? = null,
markPreviousRead: Boolean? = null
) = flow {
val response = client.submitForm<HttpResponse>(
val response = client.submitForm(
serverUrl + updateChapterRequest(mangaId, chapterIndex),
formParameters = Parameters.build {
if (read != null) {
@@ -92,7 +93,7 @@ class ChapterInteractionHandler @Inject constructor(
}
) {
method = HttpMethod.Patch
}
}.asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
@@ -129,10 +130,10 @@ class ChapterInteractionHandler @Inject constructor(
)
fun getPage(mangaId: Long, chapterIndex: Int, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = flow {
val response = client.get<ByteReadChannel>(
val response = client.get(
serverUrl + getPageQuery(mangaId, chapterIndex, pageNum),
block
)
).asSuccess().bodyAsChannel()
emit(response)
}.flowOn(Dispatchers.IO)
@@ -143,9 +144,9 @@ class ChapterInteractionHandler @Inject constructor(
fun getPage(manga: Manga, chapter: Chapter, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = getPage(manga.id, chapter.index, pageNum, block)
fun deleteChapterDownload(mangaId: Long, chapterIndex: Int) = flow {
val response = client.delete<HttpResponse>(
val response = client.delete(
serverUrl + deleteDownloadedChapterRequest(mangaId, chapterIndex)
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
@@ -156,9 +157,9 @@ class ChapterInteractionHandler @Inject constructor(
fun deleteChapterDownload(manga: Manga, chapter: Chapter) = deleteChapterDownload(manga.id, chapter.index)
fun queueChapterDownload(mangaId: Long, chapterIndex: Int) = flow {
val response = client.get<HttpResponse>(
val response = client.get(
serverUrl + queueDownloadChapterRequest(mangaId, chapterIndex)
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
@@ -169,9 +170,9 @@ class ChapterInteractionHandler @Inject constructor(
fun queueChapterDownload(manga: Manga, chapter: Chapter) = queueChapterDownload(manga.id, chapter.index)
fun stopChapterDownload(mangaId: Long, chapterIndex: Int) = flow {
val response = client.delete<HttpResponse>(
val response = client.delete(
serverUrl + stopDownloadingChapterRequest(mangaId, chapterIndex)
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
@@ -182,7 +183,7 @@ class ChapterInteractionHandler @Inject constructor(
fun stopChapterDownload(manga: Manga, chapter: Chapter) = stopChapterDownload(manga.id, chapter.index)
fun updateChapterMeta(mangaId: Long, chapterIndex: Int, key: String, value: String) = flow {
val response = client.submitForm<HttpResponse>(
val response = client.submitForm(
serverUrl + updateChapterMetaRequest(mangaId, chapterIndex),
formParameters = Parameters.build {
append("key", key)
@@ -190,7 +191,7 @@ class ChapterInteractionHandler @Inject constructor(
}
) {
method = HttpMethod.Patch
}
}.asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)

View File

@@ -6,6 +6,7 @@
package ca.gosyer.jui.data.server.interactions
import ca.gosyer.jui.core.io.asSuccess
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.server.Http
import ca.gosyer.jui.data.server.ServerPreferences
@@ -13,7 +14,6 @@ import ca.gosyer.jui.data.server.requests.downloadsClearRequest
import ca.gosyer.jui.data.server.requests.downloadsStartRequest
import ca.gosyer.jui.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
@@ -25,23 +25,23 @@ class DownloadInteractionHandler @Inject constructor(
) : BaseInteractionHandler(client, serverPreferences) {
fun startDownloading() = flow {
val response = client.get<HttpResponse>(
val response = client.get(
serverUrl + downloadsStartRequest()
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
fun stopDownloading() = flow {
val response = client.get<HttpResponse>(
val response = client.get(
serverUrl + downloadsStopRequest()
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
fun clearDownloadQueue() = flow {
val response = client.get<HttpResponse>(
val response = client.get(
serverUrl + downloadsClearRequest()
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -6,6 +6,7 @@
package ca.gosyer.jui.data.server.interactions
import ca.gosyer.jui.core.io.asSuccess
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.models.Extension
import ca.gosyer.jui.data.server.Http
@@ -15,10 +16,10 @@ import ca.gosyer.jui.data.server.requests.apkInstallQuery
import ca.gosyer.jui.data.server.requests.apkUninstallQuery
import ca.gosyer.jui.data.server.requests.apkUpdateQuery
import ca.gosyer.jui.data.server.requests.extensionListQuery
import io.ktor.client.call.body
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 io.ktor.client.statement.bodyAsChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
@@ -30,38 +31,38 @@ class ExtensionInteractionHandler @Inject constructor(
) : BaseInteractionHandler(client, serverPreferences) {
fun getExtensionList() = flow {
val response = client.get<List<Extension>>(
val response = client.get(
serverUrl + extensionListQuery()
)
).asSuccess().body<List<Extension>>()
emit(response)
}.flowOn(Dispatchers.IO)
fun installExtension(extension: Extension) = flow {
val response = client.get<HttpResponse>(
val response = client.get(
serverUrl + apkInstallQuery(extension.pkgName)
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
fun updateExtension(extension: Extension) = flow {
val response = client.get<HttpResponse>(
val response = client.get(
serverUrl + apkUpdateQuery(extension.pkgName)
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
fun uninstallExtension(extension: Extension) = flow {
val response = client.get<HttpResponse>(
val response = client.get(
serverUrl + apkUninstallQuery(extension.pkgName)
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
fun getApkIcon(extension: Extension, block: HttpRequestBuilder.() -> Unit) = flow {
val response = client.get<ByteReadChannel>(
val response = client.get(
serverUrl + apkIconQuery(extension.apkName),
block
)
).asSuccess().bodyAsChannel()
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -6,6 +6,7 @@
package ca.gosyer.jui.data.server.interactions
import ca.gosyer.jui.core.io.asSuccess
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.models.Manga
import ca.gosyer.jui.data.server.Http
@@ -14,7 +15,6 @@ import ca.gosyer.jui.data.server.requests.addMangaToLibraryQuery
import ca.gosyer.jui.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
@@ -26,18 +26,18 @@ class LibraryInteractionHandler @Inject constructor(
) : BaseInteractionHandler(client, serverPreferences) {
fun addMangaToLibrary(mangaId: Long) = flow {
val response = client.get<HttpResponse>(
val response = client.get(
serverUrl + addMangaToLibraryQuery(mangaId)
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
fun addMangaToLibrary(manga: Manga) = addMangaToLibrary(manga.id)
fun removeMangaFromLibrary(mangaId: Long) = flow {
val response = client.delete<HttpResponse>(
val response = client.delete(
serverUrl + removeMangaFromLibraryRequest(mangaId)
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)

View File

@@ -6,6 +6,7 @@
package ca.gosyer.jui.data.server.interactions
import ca.gosyer.jui.core.io.asSuccess
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.models.Manga
import ca.gosyer.jui.data.server.Http
@@ -13,14 +14,14 @@ import ca.gosyer.jui.data.server.ServerPreferences
import ca.gosyer.jui.data.server.requests.mangaQuery
import ca.gosyer.jui.data.server.requests.mangaThumbnailQuery
import ca.gosyer.jui.data.server.requests.updateMangaMetaRequest
import io.ktor.client.call.body
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.submitForm
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsChannel
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
@@ -32,7 +33,7 @@ class MangaInteractionHandler @Inject constructor(
) : BaseInteractionHandler(client, serverPreferences) {
fun getManga(mangaId: Long, refresh: Boolean = false) = flow {
val response = client.get<Manga>(
val response = client.get(
serverUrl + mangaQuery(mangaId)
) {
url {
@@ -40,22 +41,22 @@ class MangaInteractionHandler @Inject constructor(
parameter("onlineFetch", true)
}
}
}
}.asSuccess().body<Manga>()
emit(response)
}.flowOn(Dispatchers.IO)
fun getManga(manga: Manga, refresh: Boolean = false) = getManga(manga.id, refresh)
fun getMangaThumbnail(mangaId: Long, block: HttpRequestBuilder.() -> Unit) = flow {
val response = client.get<ByteReadChannel>(
val response = client.get(
serverUrl + mangaThumbnailQuery(mangaId),
block
)
).asSuccess().bodyAsChannel()
emit(response)
}.flowOn(Dispatchers.IO)
fun updateMangaMeta(mangaId: Long, key: String, value: String) = flow {
val response = client.submitForm<HttpResponse>(
val response = client.submitForm(
serverUrl + updateMangaMetaRequest(mangaId),
formParameters = Parameters.build {
append("key", key)
@@ -63,7 +64,7 @@ class MangaInteractionHandler @Inject constructor(
}
) {
method = HttpMethod.Patch
}
}.asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)

View File

@@ -6,15 +6,16 @@
package ca.gosyer.jui.data.server.interactions
import ca.gosyer.jui.core.io.asSuccess
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.models.About
import ca.gosyer.jui.data.server.Http
import ca.gosyer.jui.data.server.ServerPreferences
import ca.gosyer.jui.data.server.requests.aboutQuery
import ca.gosyer.jui.data.server.requests.checkUpdateQuery
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
@@ -26,16 +27,16 @@ class SettingsInteractionHandler @Inject constructor(
) : BaseInteractionHandler(client, serverPreferences) {
fun aboutServer() = flow {
val response = client.get<About>(
val response = client.get(
serverUrl + aboutQuery()
)
).asSuccess().body<About>()
emit(response)
}.flowOn(Dispatchers.IO)
fun checkUpdate() = flow {
val response = client.post<HttpResponse>(
val response = client.post(
serverUrl + checkUpdateQuery()
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -6,6 +6,7 @@
package ca.gosyer.jui.data.server.interactions
import ca.gosyer.jui.core.io.asSuccess
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.models.MangaPage
import ca.gosyer.jui.data.models.Source
@@ -25,10 +26,11 @@ import ca.gosyer.jui.data.server.requests.sourceListQuery
import ca.gosyer.jui.data.server.requests.sourcePopularQuery
import ca.gosyer.jui.data.server.requests.sourceSearchQuery
import ca.gosyer.jui.data.server.requests.updateSourceSettingQuery
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.request.post
import io.ktor.client.statement.HttpResponse
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.contentType
import kotlinx.coroutines.Dispatchers
@@ -44,25 +46,25 @@ class SourceInteractionHandler @Inject constructor(
) : BaseInteractionHandler(client, serverPreferences) {
fun getSourceList() = flow {
val response = client.get<List<Source>>(
val response = client.get(
serverUrl + sourceListQuery()
)
).asSuccess().body<List<Source>>()
emit(response)
}.flowOn(Dispatchers.IO)
fun getSourceInfo(sourceId: Long) = flow {
val response = client.get<Source>(
val response = client.get(
serverUrl + sourceInfoQuery(sourceId)
)
).asSuccess().body<Source>()
emit(response)
}.flowOn(Dispatchers.IO)
fun getSourceInfo(source: Source) = getSourceInfo(source.id)
fun getPopularManga(sourceId: Long, pageNum: Int) = flow {
val response = client.get<MangaPage>(
val response = client.get(
serverUrl + sourcePopularQuery(sourceId, pageNum)
)
).asSuccess().body<MangaPage>()
emit(response)
}.flowOn(Dispatchers.IO)
@@ -72,9 +74,9 @@ class SourceInteractionHandler @Inject constructor(
)
fun getLatestManga(sourceId: Long, pageNum: Int) = flow {
val response = client.get<MangaPage>(
val response = client.get(
serverUrl + sourceLatestQuery(sourceId, pageNum)
)
).asSuccess().body<MangaPage>()
emit(response)
}.flowOn(Dispatchers.IO)
@@ -85,7 +87,7 @@ class SourceInteractionHandler @Inject constructor(
// TODO: 2021-03-14
fun getGlobalSearchResults(searchTerm: String) = flow {
val response = client.get<HttpResponse>(
val response = client.get(
serverUrl + globalSearchQuery()
) {
url {
@@ -93,12 +95,12 @@ class SourceInteractionHandler @Inject constructor(
parameter("searchTerm", searchTerm)
}
}
}
}.asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
fun getSearchResults(sourceId: Long, searchTerm: String, pageNum: Int) = flow {
val response = client.get<MangaPage>(
val response = client.get(
serverUrl + sourceSearchQuery(sourceId)
) {
url {
@@ -107,7 +109,7 @@ class SourceInteractionHandler @Inject constructor(
parameter("searchTerm", searchTerm)
}
}
}
}.asSuccess().body<MangaPage>()
emit(response)
}.flowOn(Dispatchers.IO)
@@ -118,7 +120,7 @@ class SourceInteractionHandler @Inject constructor(
)
fun getFilterList(sourceId: Long, reset: Boolean = false) = flow {
val response = client.get<List<SourceFilter>>(
val response = client.get(
serverUrl + getFilterListQuery(sourceId)
) {
url {
@@ -126,19 +128,19 @@ class SourceInteractionHandler @Inject constructor(
parameter("reset", true)
}
}
}
}.asSuccess().body<List<SourceFilter>>()
emit(response)
}.flowOn(Dispatchers.IO)
fun getFilterList(source: Source, reset: Boolean = false) = getFilterList(source.id, reset)
fun setFilter(sourceId: Long, sourceFilter: SourceFilterChange) = flow {
val response = client.post<HttpResponse>(
val response = client.post(
serverUrl + setFilterRequest(sourceId)
) {
contentType(ContentType.Application.Json)
body = sourceFilter
}
setBody(sourceFilter)
}.asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
@@ -156,21 +158,21 @@ class SourceInteractionHandler @Inject constructor(
)
fun getSourceSettings(sourceId: Long) = flow {
val response = client.get<List<SourcePreference>>(
val response = client.get(
serverUrl + getSourceSettingsQuery(sourceId)
)
).asSuccess().body<List<SourcePreference>>()
emit(response)
}.flowOn(Dispatchers.IO)
fun getSourceSettings(source: Source) = getSourceSettings(source.id)
fun setSourceSetting(sourceId: Long, sourcePreference: SourcePreferenceChange) = flow {
val response = client.post<HttpResponse>(
val response = client.post(
serverUrl + updateSourceSettingQuery(sourceId)
) {
contentType(ContentType.Application.Json)
body = sourcePreference
}
setBody(sourcePreference)
}.asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)

View File

@@ -6,6 +6,7 @@
package ca.gosyer.jui.data.server.interactions
import ca.gosyer.jui.core.io.asSuccess
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.models.Category
import ca.gosyer.jui.data.models.Updates
@@ -13,10 +14,10 @@ import ca.gosyer.jui.data.server.Http
import ca.gosyer.jui.data.server.ServerPreferences
import ca.gosyer.jui.data.server.requests.fetchUpdatesRequest
import ca.gosyer.jui.data.server.requests.recentUpdatesQuery
import io.ktor.client.call.body
import io.ktor.client.request.forms.submitForm
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
@@ -29,26 +30,26 @@ class UpdatesInteractionHandler @Inject constructor(
) : BaseInteractionHandler(client, serverPreferences) {
fun getRecentUpdates(pageNum: Int) = flow {
val response = client.get<Updates>(
val response = client.get(
serverUrl + recentUpdatesQuery(pageNum)
)
).asSuccess().body<Updates>()
emit(response)
}.flowOn(Dispatchers.IO)
fun updateLibrary() = flow {
val response = client.post<HttpResponse>(
val response = client.post(
serverUrl + fetchUpdatesRequest()
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)
fun updateCategory(categoryId: Long) = flow {
val response = client.submitForm<HttpResponse>(
val response = client.submitForm(
serverUrl + fetchUpdatesRequest(),
formParameters = Parameters.build {
append("category", categoryId.toString())
}
)
).asSuccess()
emit(response)
}.flowOn(Dispatchers.IO)

View File

@@ -8,20 +8,20 @@ package ca.gosyer.jui.data.server.requests
@Post
fun backupImportRequest() =
"/api/v1/backup/import"
"api/v1/backup/import"
@Post
fun backupFileImportRequest() =
"/api/v1/backup/import/file"
"api/v1/backup/import/file"
@Post
fun backupExportRequest() =
"/api/v1/backup/export"
"api/v1/backup/export"
@Post
fun backupFileExportRequest() =
"/api/v1/backup/export/file"
"api/v1/backup/export/file"
@Post
fun validateBackupFileRequest() =
"/api/v1/backup/validate/file"
"api/v1/backup/validate/file"

View File

@@ -8,39 +8,39 @@ package ca.gosyer.jui.data.server.requests
@Get
fun getMangaCategoriesQuery(mangaId: Long) =
"/api/v1/manga/$mangaId/category/"
"api/v1/manga/$mangaId/category/"
@Get
fun addMangaToCategoryQuery(mangaId: Long, categoryId: Long) =
"/api/v1/manga/$mangaId/category/$categoryId"
"api/v1/manga/$mangaId/category/$categoryId"
@Delete
fun removeMangaFromCategoryRequest(mangaId: Long, categoryId: Long) =
"/api/v1/manga/$mangaId/category/$categoryId"
"api/v1/manga/$mangaId/category/$categoryId"
@Get
fun getCategoriesQuery() =
"/api/v1/category/"
"api/v1/category/"
/**
* Post a formbody with the param {name} for creation of a category
*/
@Post
fun createCategoryRequest() =
"/api/v1/category/"
"api/v1/category/"
@Patch
fun categoryModifyRequest(categoryId: Long) =
"/api/v1/category/$categoryId"
"api/v1/category/$categoryId"
@Patch
fun categoryReorderRequest() =
"/api/v1/category/reorder"
"api/v1/category/reorder"
@Delete
fun categoryDeleteRequest(categoryId: Long) =
"/api/v1/category/$categoryId"
"api/v1/category/$categoryId"
@Get
fun getMangaInCategoryQuery(categoryId: Long) =
"/api/v1/category/$categoryId"
"api/v1/category/$categoryId"

View File

@@ -8,32 +8,32 @@ package ca.gosyer.jui.data.server.requests
@Get
fun getMangaChaptersQuery(mangaId: Long) =
"/api/v1/manga/$mangaId/chapters"
"api/v1/manga/$mangaId/chapters"
@Get
fun getChapterQuery(mangaId: Long, chapterIndex: Int) =
"/api/v1/manga/$mangaId/chapter/$chapterIndex"
"api/v1/manga/$mangaId/chapter/$chapterIndex"
@Patch
fun updateChapterRequest(mangaId: Long, chapterIndex: Int) =
"/api/v1/manga/$mangaId/chapter/$chapterIndex"
"api/v1/manga/$mangaId/chapter/$chapterIndex"
@Get
fun getPageQuery(mangaId: Long, chapterIndex: Int, index: Int) =
"/api/v1/manga/$mangaId/chapter/$chapterIndex/page/$index"
"api/v1/manga/$mangaId/chapter/$chapterIndex/page/$index"
@Delete
fun deleteDownloadedChapterRequest(mangaId: Long, chapterIndex: Int) =
"/api/v1/manga/$mangaId/chapter/$chapterIndex"
"api/v1/manga/$mangaId/chapter/$chapterIndex"
@Get
fun queueDownloadChapterRequest(mangaId: Long, chapterIndex: Int) =
"/api/v1/download/$mangaId/chapter/$chapterIndex"
"api/v1/download/$mangaId/chapter/$chapterIndex"
@Delete
fun stopDownloadingChapterRequest(mangaId: Long, chapterIndex: Int) =
"/api/v1/download/$mangaId/chapter/$chapterIndex"
"api/v1/download/$mangaId/chapter/$chapterIndex"
@Patch
fun updateChapterMetaRequest(mangaId: Long, chapterIndex: Int) =
"/api/v1/manga/$mangaId/chapter/$chapterIndex/meta"
"api/v1/manga/$mangaId/chapter/$chapterIndex/meta"

View File

@@ -8,16 +8,16 @@ package ca.gosyer.jui.data.server.requests
@WS
fun downloadsQuery() =
"/api/v1/downloads"
"api/v1/downloads"
@Get
fun downloadsStartRequest() =
"/api/v1/downloads/start"
"api/v1/downloads/start"
@Get
fun downloadsStopRequest() =
"/api/v1/downloads/stop"
"api/v1/downloads/stop"
@Get
fun downloadsClearRequest() =
"/api/v1/downloads/clear"
"api/v1/downloads/clear"

View File

@@ -8,20 +8,20 @@ package ca.gosyer.jui.data.server.requests
@Get
fun extensionListQuery() =
"/api/v1/extension/list"
"api/v1/extension/list"
@Get
fun apkInstallQuery(pkgName: String) =
"/api/v1/extension/install/$pkgName"
"api/v1/extension/install/$pkgName"
@Get
fun apkUpdateQuery(pkgName: String) =
"/api/v1/extension/update/$pkgName"
"api/v1/extension/update/$pkgName"
@Get
fun apkUninstallQuery(pkgName: String) =
"/api/v1/extension/uninstall/$pkgName"
"api/v1/extension/uninstall/$pkgName"
@Get
fun apkIconQuery(apkName: String) =
"/api/v1/extension/icon/$apkName"
"api/v1/extension/icon/$apkName"

View File

@@ -8,8 +8,8 @@ package ca.gosyer.jui.data.server.requests
@Get
fun addMangaToLibraryQuery(mangaId: Long) =
"/api/v1/manga/$mangaId/library"
"api/v1/manga/$mangaId/library"
@Delete
fun removeMangaFromLibraryRequest(mangaId: Long) =
"/api/v1/manga/$mangaId/library"
"api/v1/manga/$mangaId/library"

View File

@@ -8,12 +8,12 @@ package ca.gosyer.jui.data.server.requests
@Get
fun mangaQuery(mangaId: Long) =
"/api/v1/manga/$mangaId/"
"api/v1/manga/$mangaId/"
@Get
fun mangaThumbnailQuery(mangaId: Long) =
"/api/v1/manga/$mangaId/thumbnail"
"api/v1/manga/$mangaId/thumbnail"
@Post
fun updateMangaMetaRequest(mangaId: Long) =
"/api/v1/manga/$mangaId/meta"
"api/v1/manga/$mangaId/meta"

View File

@@ -8,8 +8,8 @@ package ca.gosyer.jui.data.server.requests
@Get
fun aboutQuery() =
"/api/v1/settings/about"
"api/v1/settings/about"
@Get
fun checkUpdateQuery() =
"/api/v1/settings/check-update"
"api/v1/settings/check-update"

View File

@@ -8,40 +8,40 @@ package ca.gosyer.jui.data.server.requests
@Get
fun sourceListQuery() =
"/api/v1/source/list"
"api/v1/source/list"
@Get
fun sourceInfoQuery(sourceId: Long) =
"/api/v1/source/$sourceId"
"api/v1/source/$sourceId"
@Get
fun sourcePopularQuery(sourceId: Long, pageNum: Int) =
"/api/v1/source/$sourceId/popular/$pageNum"
"api/v1/source/$sourceId/popular/$pageNum"
@Get
fun sourceLatestQuery(sourceId: Long, pageNum: Int) =
"/api/v1/source/$sourceId/latest/$pageNum"
"api/v1/source/$sourceId/latest/$pageNum"
@Get
fun globalSearchQuery() =
"/api/v1/source/all/search"
"api/v1/source/all/search"
@Get
fun sourceSearchQuery(sourceId: Long) =
"/api/v1/source/$sourceId/search"
"api/v1/source/$sourceId/search"
@Get
fun getFilterListQuery(sourceId: Long) =
"/api/v1/source/$sourceId/filters"
"api/v1/source/$sourceId/filters"
@Post
fun setFilterRequest(sourceId: Long) =
"/api/v1/source/$sourceId/filters"
"api/v1/source/$sourceId/filters"
@Get
fun getSourceSettingsQuery(sourceId: Long) =
"/api/v1/source/$sourceId/preferences"
"api/v1/source/$sourceId/preferences"
@Post
fun updateSourceSettingQuery(sourceId: Long) =
"/api/v1/source/$sourceId/preferences"
"api/v1/source/$sourceId/preferences"

View File

@@ -8,16 +8,16 @@ package ca.gosyer.jui.data.server.requests
@Get
fun recentUpdatesQuery(pageNum: Int) =
"/api/v1/update/recentChapters/$pageNum"
"api/v1/update/recentChapters/$pageNum"
@Post
fun fetchUpdatesRequest() =
"/api/v1/update/fetch"
"api/v1/update/fetch"
@Get
fun updatesSummaryQuery() =
"/api/v1/update/summary"
"api/v1/update/summary"
@WS
fun updatesQuery() =
"/api/v1/update"
"api/v1/update"

View File

@@ -10,6 +10,7 @@ import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.build.BuildKonfig
import ca.gosyer.jui.data.server.Http
import ca.gosyer.jui.data.update.model.GithubRelease
import io.ktor.client.call.body
import io.ktor.client.request.get
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
@@ -22,9 +23,9 @@ class UpdateChecker @Inject constructor(
) {
fun checkForUpdates() = flow {
// if (!updatePreferences.enabled().get()) return
val latestRelease = client.get<GithubRelease>(
val latestRelease = client.get(
"https://api.github.com/repos/$GITHUB_REPO/releases/latest"
)
).body<GithubRelease>()
if (isNewVersion(latestRelease.version)) {
emit(Update.UpdateFound(latestRelease))

View File

@@ -59,7 +59,8 @@ dependencies {
// Http client
implementation(libs.ktor.core)
implementation(libs.ktor.okHttp)
implementation(libs.ktor.serialization)
implementation(libs.ktor.contentNegotiation)
implementation(libs.ktor.serialization.json)
implementation(libs.ktor.logging)
implementation(libs.ktor.websockets)
implementation(libs.ktor.auth)

View File

@@ -32,7 +32,7 @@ ksp = "1.6.10-1.0.4"
kotlinInject = "0.4.1"
# Network
ktor = "1.6.8"
ktor = "2.0.0"
# Logging
slf4j = "1.7.36"
@@ -113,7 +113,8 @@ kotlinInject-compiler = { module = "me.tatarka.inject:kotlin-inject-compiler-ksp
# Network
ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-okHttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-serialization = { module = "io.ktor:ktor-client-serialization", version.ref = "ktor" }
ktor-contentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
ktor-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
ktor-auth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" }

View File

@@ -6,6 +6,7 @@
package ca.gosyer.jui.ui.base.image
import ca.gosyer.jui.core.io.HttpException
import ca.gosyer.jui.data.models.Extension
import ca.gosyer.jui.data.models.Manga
import ca.gosyer.jui.data.models.Source
@@ -25,16 +26,16 @@ import io.kamel.core.fetcher.Fetcher
import io.kamel.core.mapper.Mapper
import io.kamel.image.config.imageBitmapDecoder
import io.ktor.client.HttpClient
import io.ktor.client.call.receive
import io.ktor.client.features.onDownload
import io.ktor.client.plugins.onDownload
import io.ktor.client.request.request
import io.ktor.client.request.takeFrom
import io.ktor.client.request.url
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsChannel
import io.ktor.client.statement.discardRemaining
import io.ktor.http.Url
import io.ktor.http.isSuccess
import io.ktor.utils.io.ByteReadChannel
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -70,19 +71,19 @@ class KamelConfigProvider @Inject constructor(
class MangaCoverMapper(private val serverUrlStateFlow: StateFlow<Url>) : Mapper<Manga, Url> {
override fun map(input: Manga): Url {
return Url(serverUrlStateFlow.value.toString() + input.thumbnailUrl)
return Url(serverUrlStateFlow.value.toString() + input.thumbnailUrl?.trimStart('/'))
}
}
class ExtensionIconMapper(private val serverUrlStateFlow: StateFlow<Url>) : Mapper<Extension, Url> {
override fun map(input: Extension): Url {
return Url(serverUrlStateFlow.value.toString() + input.iconUrl)
return Url(serverUrlStateFlow.value.toString() + input.iconUrl.trimStart('/'))
}
}
class SourceIconMapper(private val serverUrlStateFlow: StateFlow<Url>) : Mapper<Source, Url> {
override fun map(input: Source): Url {
return Url(serverUrlStateFlow.value.toString() + input.iconUrl)
return Url(serverUrlStateFlow.value.toString() + input.iconUrl.trimStart('/'))
}
}
@@ -93,12 +94,11 @@ class KamelConfigProvider @Inject constructor(
override val Url.isSupported: Boolean
get() = protocol.name == "https" || protocol.name == "http"
@OptIn(ExperimentalCoroutinesApi::class)
override fun fetch(
data: Url,
resourceConfig: ResourceConfig
): Flow<Resource<ByteReadChannel>> = channelFlow {
val response = client.request<HttpResponse> {
val response = client.request {
onDownload { bytesSentTotal, contentLength ->
val progress = (bytesSentTotal.toFloat() / contentLength).coerceAtMost(1.0F)
send(Resource.Loading(progress))
@@ -106,8 +106,13 @@ class KamelConfigProvider @Inject constructor(
takeFrom(resourceConfig.requestData)
url(data)
}
val bytes = response.receive<ByteReadChannel>()
send(Resource.Success(bytes))
if (response.status.isSuccess()) {
val bytes = response.bodyAsChannel()
send(Resource.Success(bytes))
} else {
response.discardRemaining()
send(Resource.Failure(HttpException(response.status)))
}
}
}
}

View File

@@ -15,7 +15,7 @@ import ca.gosyer.jui.ui.reader.model.ReaderPage
import ca.gosyer.jui.ui.util.compose.toImageBitmap
import ca.gosyer.jui.ui.util.lang.priorityChannel
import cafe.adriel.voyager.core.concurrent.AtomicInt32
import io.ktor.client.features.onDownload
import io.ktor.client.plugins.onDownload
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob

View File

@@ -59,9 +59,9 @@ import com.vanpra.composematerialdialogs.MaterialDialog
import com.vanpra.composematerialdialogs.MaterialDialogState
import com.vanpra.composematerialdialogs.rememberMaterialDialogState
import com.vanpra.composematerialdialogs.title
import io.ktor.client.features.onDownload
import io.ktor.client.features.onUpload
import io.ktor.http.isSuccess
import io.ktor.client.plugins.onDownload
import io.ktor.client.plugins.onUpload
import io.ktor.client.statement.bodyAsChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -202,27 +202,25 @@ class SettingsBackupViewModel @Inject constructor(
}
}
.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.toSource().saveTo(it)
} catch (e: Exception) {
e.throwIfCancellation()
log.warn(e) { "Error creating backup" }
_creatingStatus.value = Status.Error
_creating.value = false
} finally {
mutex.unlock()
}
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.bodyAsChannel().toSource().saveTo(it)
} catch (e: Exception) {
e.throwIfCancellation()
log.warn(e) { "Error creating backup" }
_creatingStatus.value = Status.Error
_creating.value = false
} finally {
mutex.unlock()
}
}
_createFlow.emit(filename)
}
_createFlow.emit(filename)
}
.catch {
log.warn(it) { "Error exporting backup" }

View File

@@ -8,9 +8,11 @@ package ca.gosyer.jui.ui.util.compose
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap
import ca.gosyer.jui.core.io.asSuccess
import ca.gosyer.jui.data.server.Http
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsChannel
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.jvm.javaio.copyTo
import okio.FileSystem
@@ -25,7 +27,7 @@ fun imageFromFile(file: Path): ImageBitmap {
}
suspend fun imageFromUrl(client: Http, url: String, block: HttpRequestBuilder.() -> Unit): ImageBitmap {
return client.get<ByteReadChannel>(url, block).toImageBitmap()
return client.get(url, block).asSuccess().bodyAsChannel().toImageBitmap()
}
actual suspend fun ByteReadChannel.toImageBitmap(): ImageBitmap {