Fixes for Manga page with GraphQL

This commit is contained in:
Syer10
2025-10-03 21:00:20 -04:00
parent e171d10588
commit 331acb966a
25 changed files with 97 additions and 808 deletions

View File

@@ -1,50 +1,4 @@
package ca.gosyer.jui.data package ca.gosyer.jui.data
import ca.gosyer.jui.domain.backup.service.createBackupRepositoryOld
import ca.gosyer.jui.domain.category.service.createCategoryRepositoryOld
import ca.gosyer.jui.domain.chapter.service.createChapterRepositoryOld
import ca.gosyer.jui.domain.download.service.createDownloadRepositoryOld
import ca.gosyer.jui.domain.extension.service.createExtensionRepositoryOld
import ca.gosyer.jui.domain.global.service.createGlobalRepositoryOld
import ca.gosyer.jui.domain.library.service.createLibraryRepositoryOld
import ca.gosyer.jui.domain.manga.service.createMangaRepositoryOld
import ca.gosyer.jui.domain.settings.service.createSettingsRepositoryOld
import ca.gosyer.jui.domain.source.service.createSourceRepositoryOld
import ca.gosyer.jui.domain.updates.service.createUpdatesRepositoryOld
import de.jensklingenberg.ktorfit.Ktorfit
import me.tatarka.inject.annotations.Provides
actual interface SharedDataComponent { actual interface SharedDataComponent {
@Provides
fun backupRepositoryOld(ktorfit: Ktorfit) = ktorfit.createBackupRepositoryOld()
@Provides
fun categoryRepositoryOld(ktorfit: Ktorfit) = ktorfit.createCategoryRepositoryOld()
@Provides
fun chapterRepositoryOld(ktorfit: Ktorfit) = ktorfit.createChapterRepositoryOld()
@Provides
fun downloadRepositoryOld(ktorfit: Ktorfit) = ktorfit.createDownloadRepositoryOld()
@Provides
fun extensionRepositoryOld(ktorfit: Ktorfit) = ktorfit.createExtensionRepositoryOld()
@Provides
fun globalRepositoryOld(ktorfit: Ktorfit) = ktorfit.createGlobalRepositoryOld()
@Provides
fun libraryRepositoryOld(ktorfit: Ktorfit) = ktorfit.createLibraryRepositoryOld()
@Provides
fun mangaRepositoryOld(ktorfit: Ktorfit) = ktorfit.createMangaRepositoryOld()
@Provides
fun settingsRepositoryOld(ktorfit: Ktorfit) = ktorfit.createSettingsRepositoryOld()
@Provides
fun sourceRepositoryOld(ktorfit: Ktorfit) = ktorfit.createSourceRepositoryOld()
@Provides
fun updatesRepositoryOld(ktorfit: Ktorfit) = ktorfit.createUpdatesRepositoryOld()
} }

View File

@@ -229,3 +229,14 @@ mutation SetSettings(
clientMutationId clientMutationId
} }
} }
query AboutServer {
aboutServer {
buildTime
buildType
discord
github
name
version
}
}

View File

@@ -6,6 +6,7 @@
package ca.gosyer.jui.data.settings package ca.gosyer.jui.data.settings
import ca.gosyer.jui.data.graphql.AboutServerQuery
import ca.gosyer.jui.data.graphql.AllSettingsQuery import ca.gosyer.jui.data.graphql.AllSettingsQuery
import ca.gosyer.jui.data.graphql.SetSettingsMutation import ca.gosyer.jui.data.graphql.SetSettingsMutation
import ca.gosyer.jui.data.graphql.fragment.SettingsTypeFragment import ca.gosyer.jui.data.graphql.fragment.SettingsTypeFragment
@@ -20,6 +21,8 @@ import ca.gosyer.jui.data.graphql.type.WebUIChannel
import ca.gosyer.jui.data.graphql.type.WebUIFlavor import ca.gosyer.jui.data.graphql.type.WebUIFlavor
import ca.gosyer.jui.data.graphql.type.WebUIInterface import ca.gosyer.jui.data.graphql.type.WebUIInterface
import ca.gosyer.jui.data.util.toOptional import ca.gosyer.jui.data.util.toOptional
import ca.gosyer.jui.domain.settings.model.About
import ca.gosyer.jui.domain.settings.model.AboutBuildType
import ca.gosyer.jui.domain.settings.model.SetSettingsInput import ca.gosyer.jui.domain.settings.model.SetSettingsInput
import ca.gosyer.jui.domain.settings.model.Settings import ca.gosyer.jui.domain.settings.model.Settings
import ca.gosyer.jui.domain.settings.service.SettingsRepository import ca.gosyer.jui.domain.settings.service.SettingsRepository
@@ -350,4 +353,26 @@ class SettingsRepositoryImpl(
Unit Unit
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
override fun aboutServer(): Flow<About> {
return apolloClient.query(
AboutServerQuery()
)
.toFlow()
.map {
val data = it.dataAssertNoErrors
About(
data.aboutServer.name,
data.aboutServer.version,
when (data.aboutServer.buildType) {
"Preview" -> AboutBuildType.Preview
else -> AboutBuildType.Stable
},
data.aboutServer.buildTime,
data.aboutServer.github,
data.aboutServer.discord
)
}
.flowOn(Dispatchers.IO)
}
} }

View File

@@ -1,50 +1,5 @@
package ca.gosyer.jui.data package ca.gosyer.jui.data
import ca.gosyer.jui.domain.backup.service.createBackupRepositoryOld
import ca.gosyer.jui.domain.category.service.createCategoryRepositoryOld
import ca.gosyer.jui.domain.chapter.service.createChapterRepositoryOld
import ca.gosyer.jui.domain.download.service.createDownloadRepositoryOld
import ca.gosyer.jui.domain.extension.service.createExtensionRepositoryOld
import ca.gosyer.jui.domain.global.service.createGlobalRepositoryOld
import ca.gosyer.jui.domain.library.service.createLibraryRepositoryOld
import ca.gosyer.jui.domain.manga.service.createMangaRepositoryOld
import ca.gosyer.jui.domain.settings.service.createSettingsRepositoryOld
import ca.gosyer.jui.domain.source.service.createSourceRepositoryOld
import ca.gosyer.jui.domain.updates.service.createUpdatesRepositoryOld
import de.jensklingenberg.ktorfit.Ktorfit
import me.tatarka.inject.annotations.Provides
actual interface SharedDataComponent { actual interface SharedDataComponent {
@Provides
fun backupRepositoryOld(ktorfit: Ktorfit) = ktorfit.createBackupRepositoryOld()
@Provides
fun categoryRepositoryOld(ktorfit: Ktorfit) = ktorfit.createCategoryRepositoryOld()
@Provides
fun chapterRepositoryOld(ktorfit: Ktorfit) = ktorfit.createChapterRepositoryOld()
@Provides
fun downloadRepositoryOld(ktorfit: Ktorfit) = ktorfit.createDownloadRepositoryOld()
@Provides
fun extensionRepositoryOld(ktorfit: Ktorfit) = ktorfit.createExtensionRepositoryOld()
@Provides
fun globalRepositoryOld(ktorfit: Ktorfit) = ktorfit.createGlobalRepositoryOld()
@Provides
fun libraryRepositoryOld(ktorfit: Ktorfit) = ktorfit.createLibraryRepositoryOld()
@Provides
fun mangaRepositoryOld(ktorfit: Ktorfit) = ktorfit.createMangaRepositoryOld()
@Provides
fun settingsRepositoryOld(ktorfit: Ktorfit) = ktorfit.createSettingsRepositoryOld()
@Provides
fun sourceRepositoryOld(ktorfit: Ktorfit) = ktorfit.createSourceRepositoryOld()
@Provides
fun updatesRepositoryOld(ktorfit: Ktorfit) = ktorfit.createUpdatesRepositoryOld()
} }

View File

@@ -1,50 +1,5 @@
package ca.gosyer.jui.data package ca.gosyer.jui.data
import ca.gosyer.jui.domain.backup.service.createBackupRepositoryOld
import ca.gosyer.jui.domain.category.service.createCategoryRepositoryOld
import ca.gosyer.jui.domain.chapter.service.createChapterRepositoryOld
import ca.gosyer.jui.domain.download.service.createDownloadRepositoryOld
import ca.gosyer.jui.domain.extension.service.createExtensionRepositoryOld
import ca.gosyer.jui.domain.global.service.createGlobalRepositoryOld
import ca.gosyer.jui.domain.library.service.createLibraryRepositoryOld
import ca.gosyer.jui.domain.manga.service.createMangaRepositoryOld
import ca.gosyer.jui.domain.settings.service.createSettingsRepositoryOld
import ca.gosyer.jui.domain.source.service.createSourceRepositoryOld
import ca.gosyer.jui.domain.updates.service.createUpdatesRepositoryOld
import de.jensklingenberg.ktorfit.Ktorfit
import me.tatarka.inject.annotations.Provides
actual interface SharedDataComponent { actual interface SharedDataComponent {
@Provides
fun backupRepositoryOld(ktorfit: Ktorfit) = ktorfit.createBackupRepositoryOld()
@Provides
fun categoryRepositoryOld(ktorfit: Ktorfit) = ktorfit.createCategoryRepositoryOld()
@Provides
fun chapterRepositoryOld(ktorfit: Ktorfit) = ktorfit.createChapterRepositoryOld()
@Provides
fun downloadRepositoryOld(ktorfit: Ktorfit) = ktorfit.createDownloadRepositoryOld()
@Provides
fun extensionRepositoryOld(ktorfit: Ktorfit) = ktorfit.createExtensionRepositoryOld()
@Provides
fun globalRepositoryOld(ktorfit: Ktorfit) = ktorfit.createGlobalRepositoryOld()
@Provides
fun libraryRepositoryOld(ktorfit: Ktorfit) = ktorfit.createLibraryRepositoryOld()
@Provides
fun mangaRepositoryOld(ktorfit: Ktorfit) = ktorfit.createMangaRepositoryOld()
@Provides
fun settingsRepositoryOld(ktorfit: Ktorfit) = ktorfit.createSettingsRepositoryOld()
@Provides
fun sourceRepositoryOld(ktorfit: Ktorfit) = ktorfit.createSourceRepositoryOld()
@Provides
fun updatesRepositoryOld(ktorfit: Ktorfit) = ktorfit.createUpdatesRepositoryOld()
} }

View File

@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
@@ -39,6 +40,11 @@ class ServerListeners
) )
val chapterIdsListener = _chapterIdsListener.asSharedFlow() val chapterIdsListener = _chapterIdsListener.asSharedFlow()
private val _mangaChapterIdsListener = MutableSharedFlow<List<Long>>(
extraBufferCapacity = Channel.UNLIMITED,
)
val mangaChapterIdsListener = _mangaChapterIdsListener.asSharedFlow()
private val categoryMangaListener = MutableSharedFlow<Long>( private val categoryMangaListener = MutableSharedFlow<Long>(
extraBufferCapacity = Channel.UNLIMITED, extraBufferCapacity = Channel.UNLIMITED,
) )
@@ -85,13 +91,16 @@ class ServerListeners
fun <T> combineChapters( fun <T> combineChapters(
flow: Flow<T>, flow: Flow<T>,
idPredate: (suspend (List<Long>) -> Boolean)? = null, chapterIdPredate: (suspend (List<Long>) -> Boolean)? = null,
mangaIdPredate: (suspend (List<Long>) -> Boolean)? = null,
): Flow<T> { ): Flow<T> {
val idsListener = if (idPredate != null) { val idsListener = _chapterIdsListener
_chapterIdsListener.filter { idPredate(it) }.startWith(Unit) .filter { chapterIdPredate?.invoke(it) ?: false }
} else { .startWith(Unit)
_chapterIdsListener.startWith(Unit) .combine(
} _mangaChapterIdsListener.filter { mangaIdPredate?.invoke(it) ?: false }
.startWith(Unit)
) { _, _ -> }
return idsListener return idsListener
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)

View File

@@ -1,61 +0,0 @@
/*
* 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.domain.backup.service
import ca.gosyer.jui.core.io.SYSTEM
import ca.gosyer.jui.domain.backup.model.BackupValidationResult
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Multipart
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Part
import de.jensklingenberg.ktorfit.http.ReqBuilder
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.formData
import io.ktor.client.statement.HttpResponse
import io.ktor.http.ContentType
import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
import io.ktor.http.content.PartData
import kotlinx.coroutines.flow.Flow
import okio.FileSystem
import okio.Path
import okio.buffer
interface BackupRepositoryOld {
@Multipart
@POST("api/v1/backup/import/file")
fun importBackupFile(
@Part("") formData: List<PartData>,
@ReqBuilder block: HttpRequestBuilder.() -> Unit = {},
): Flow<HttpResponse>
@Multipart
@POST("api/v1/backup/validate/file")
fun validateBackupFile(
@Part("") formData: List<PartData>,
@ReqBuilder block: HttpRequestBuilder.() -> Unit = {},
): Flow<BackupValidationResult>
@GET("api/v1/backup/export/file")
fun exportBackupFile(
@ReqBuilder block: HttpRequestBuilder.() -> Unit = {},
): Flow<HttpResponse>
companion object {
fun buildBackupFormData(file: Path) =
formData {
append(
"backup.proto.gz",
FileSystem.SYSTEM.source(file).buffer().readByteArray(),
Headers.build {
append(HttpHeaders.ContentType, ContentType.MultiPart.FormData.toString())
append(HttpHeaders.ContentDisposition, "filename=backup.proto.gz")
},
)
}
}
}

View File

@@ -1,79 +0,0 @@
/*
* 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.domain.category.service
import ca.gosyer.jui.domain.category.model.Category
import ca.gosyer.jui.domain.manga.model.Manga
import de.jensklingenberg.ktorfit.http.DELETE
import de.jensklingenberg.ktorfit.http.Field
import de.jensklingenberg.ktorfit.http.FormUrlEncoded
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.PATCH
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Path
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
interface CategoryRepositoryOld {
@GET("api/v1/manga/{mangaId}/category/")
fun getMangaCategories(
@Path("mangaId") mangaId: Long,
): Flow<List<Category>>
@GET("api/v1/manga/{mangaId}/category/{categoryId}")
fun addMangaToCategory(
@Path("mangaId") mangaId: Long,
@Path("categoryId") categoryId: Long,
): Flow<HttpResponse>
@DELETE("api/v1/manga/{mangaId}/category/{categoryId}")
fun removeMangaFromCategory(
@Path("mangaId") mangaId: Long,
@Path("categoryId") categoryId: Long,
): Flow<HttpResponse>
@GET("api/v1/category/")
fun getCategories(): Flow<List<Category>>
@FormUrlEncoded
@POST("api/v1/category/")
fun createCategory(
@Field("name") name: String,
): Flow<HttpResponse>
@FormUrlEncoded
@PATCH("api/v1/category/{categoryId}")
fun modifyCategory(
@Path("categoryId") categoryId: Long,
@Field("name") name: String,
): Flow<HttpResponse>
@FormUrlEncoded
@PATCH("api/v1/category/reorder")
fun reorderCategory(
@Field("to") to: Int,
@Field("from") from: Int,
): Flow<HttpResponse>
@DELETE("api/v1/category/{categoryId}")
fun deleteCategory(
@Path("categoryId") categoryId: Long,
): Flow<HttpResponse>
@GET("api/v1/category/{categoryId}")
fun getMangaFromCategory(
@Path("categoryId") categoryId: Long,
): Flow<List<Manga>>
@FormUrlEncoded
@PATCH("api/v1/category/{categoryId}/meta")
fun updateCategoryMeta(
@Path("categoryId") categoryId: Long,
@Field("key") key: String,
@Field("value") value: String,
): Flow<HttpResponse>
}

View File

@@ -47,13 +47,13 @@ class GetChapter
chapterId: Long, chapterId: Long,
) = serverListeners.combineChapters( ) = serverListeners.combineChapters(
chapterRepository.getChapter(chapterId), chapterRepository.getChapter(chapterId),
idPredate = { ids -> chapterId in ids }, chapterIdPredate = { ids -> chapterId in ids },
) )
fun asFlow(chapter: Chapter) = fun asFlow(chapter: Chapter) =
serverListeners.combineChapters( serverListeners.combineChapters(
chapterRepository.getChapter(chapter.id), chapterRepository.getChapter(chapter.id),
idPredate = { ids -> chapter.id in ids }, chapterIdPredate = { ids -> chapter.id in ids },
) )
companion object { companion object {

View File

@@ -46,13 +46,13 @@ class GetChapters
fun asFlow(mangaId: Long) = fun asFlow(mangaId: Long) =
serverListeners.combineChapters( serverListeners.combineChapters(
chapterRepository.getChapters(mangaId), chapterRepository.getChapters(mangaId),
idPredate = { ids -> false }, // todo chapterIdPredate = { ids -> false }, // todo
) )
fun asFlow(manga: Manga) = fun asFlow(manga: Manga) =
serverListeners.combineChapters( serverListeners.combineChapters(
chapterRepository.getChapters(manga.id), chapterRepository.getChapters(manga.id),
idPredate = { ids -> false }, // todo chapterIdPredate = { ids -> false }, // todo
) )
companion object { companion object {

View File

@@ -1,86 +0,0 @@
/*
* 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.domain.chapter.service
import ca.gosyer.jui.domain.chapter.model.Chapter
import ca.gosyer.jui.domain.chapter.model.ChapterBatchEditInput
import ca.gosyer.jui.domain.chapter.model.MangaChapterBatchEditInput
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.DELETE
import de.jensklingenberg.ktorfit.http.Field
import de.jensklingenberg.ktorfit.http.FormUrlEncoded
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Headers
import de.jensklingenberg.ktorfit.http.PATCH
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Path
import de.jensklingenberg.ktorfit.http.Query
import de.jensklingenberg.ktorfit.http.ReqBuilder
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
interface ChapterRepositoryOld {
@GET("api/v1/manga/{mangaId}/chapters")
fun getChapters(
@Path("mangaId") mangaId: Long,
@Query("onlineFetch") refresh: Boolean = false,
): Flow<List<Chapter>>
@GET("api/v1/manga/{mangaId}/chapter/{chapterIndex}")
fun getChapter(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int,
): Flow<Chapter>
@FormUrlEncoded
@PATCH("api/v1/manga/{mangaId}/chapter/{chapterIndex}")
fun updateChapter(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int,
@Field("read") read: Boolean? = null,
@Field("bookmarked") bookmarked: Boolean? = null,
@Field("lastPageRead") lastPageRead: Int? = null,
@Field("markPrevRead") markPreviousRead: Boolean? = null,
): Flow<HttpResponse>
@POST("api/v1/manga/{mangaId}/chapter/batch")
@Headers("Content-Type: application/json")
fun batchUpdateChapter(
@Path("mangaId") mangaId: Long,
@Body input: MangaChapterBatchEditInput,
): Flow<HttpResponse>
@POST("api/v1/chapter/batch")
@Headers("Content-Type: application/json")
fun batchUpdateChapter(
@Body input: ChapterBatchEditInput,
): Flow<HttpResponse>
@GET("api/v1/manga/{mangaId}/chapter/{chapterIndex}/page/{pageNum}")
fun getPage(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int,
@Path("pageNum") pageNum: Int,
@ReqBuilder block: HttpRequestBuilder.() -> Unit,
): Flow<HttpResponse>
@DELETE("api/v1/manga/{mangaId}/chapter/{chapterIndex}")
fun deleteChapterDownload(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int,
): Flow<HttpResponse>
@FormUrlEncoded
@PATCH("api/v1/manga/{mangaId}/chapter/{chapterIndex}/meta")
fun updateChapterMeta(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int,
@Field("key") key: String,
@Field("value") value: String,
): Flow<HttpResponse>
}

View File

@@ -1,54 +0,0 @@
/*
* 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.domain.download.service
import ca.gosyer.jui.domain.download.model.DownloadEnqueue
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.DELETE
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Headers
import de.jensklingenberg.ktorfit.http.PATCH
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Path
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
interface DownloadRepositoryOld {
@GET("api/v1/downloads/start")
fun startDownloading(): Flow<HttpResponse>
@GET("api/v1/downloads/stop")
fun stopDownloading(): Flow<HttpResponse>
@GET("api/v1/downloads/clear")
fun clearDownloadQueue(): Flow<HttpResponse>
@GET("api/v1/download/{mangaId}/chapter/{chapterIndex}")
fun queueChapterDownload(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int,
): Flow<HttpResponse>
@DELETE("api/v1/download/{mangaId}/chapter/{chapterIndex}")
fun stopChapterDownload(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int,
): Flow<HttpResponse>
@PATCH("api/v1/download/{mangaId}/chapter/{chapterIndex}/reorder/{to}")
fun reorderChapterDownload(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int,
@Path("to") to: Int,
): Flow<HttpResponse>
@POST("api/v1/download/batch")
@Headers("Content-Type: application/json")
fun batchDownload(
@Body downloadEnqueue: DownloadEnqueue,
): Flow<HttpResponse>
}

View File

@@ -1,73 +0,0 @@
/*
* 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.domain.extension.service
import ca.gosyer.jui.core.io.SYSTEM
import ca.gosyer.jui.domain.extension.model.Extension
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Multipart
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Part
import de.jensklingenberg.ktorfit.http.Path
import de.jensklingenberg.ktorfit.http.ReqBuilder
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.formData
import io.ktor.client.statement.HttpResponse
import io.ktor.http.ContentType
import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
import io.ktor.http.content.PartData
import io.ktor.utils.io.ByteReadChannel
import kotlinx.coroutines.flow.Flow
import okio.FileSystem
import okio.buffer
interface ExtensionRepositoryOld {
@GET("api/v1/extension/list")
fun getExtensionList(): Flow<List<Extension>>
@Multipart
@POST("api/v1/extension/install")
fun installExtension(
@Part("") formData: List<PartData>,
): Flow<HttpResponse>
@GET("api/v1/extension/install/{pkgName}")
fun installExtension(
@Path("pkgName") pkgName: String,
): Flow<HttpResponse>
@GET("api/v1/extension/update/{pkgName}")
fun updateExtension(
@Path("pkgName") pkgName: String,
): Flow<HttpResponse>
@GET("api/v1/extension/uninstall/{pkgName}")
fun uninstallExtension(
@Path("pkgName") pkgName: String,
): Flow<HttpResponse>
@GET("api/v1/extension/icon/{apkName}")
fun getApkIcon(
@Path("apkName") apkName: String,
@ReqBuilder block: HttpRequestBuilder.() -> Unit,
): Flow<ByteReadChannel>
companion object {
fun buildExtensionFormData(file: okio.Path) =
formData {
append(
"file",
FileSystem.SYSTEM.source(file).buffer().readByteArray(),
Headers.build {
append(HttpHeaders.ContentType, ContentType.MultiPart.FormData.toString())
append(HttpHeaders.ContentDisposition, "filename=file")
},
)
}
}
}

View File

@@ -1,27 +0,0 @@
/*
* 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.domain.global.service
import ca.gosyer.jui.domain.global.model.GlobalMeta
import de.jensklingenberg.ktorfit.http.Field
import de.jensklingenberg.ktorfit.http.FormUrlEncoded
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.PATCH
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
interface GlobalRepositoryOld {
@GET("api/v1/meta")
fun getGlobalMeta(): Flow<GlobalMeta>
@FormUrlEncoded
@PATCH("api/v1/meta")
fun updateGlobalMeta(
@Field("key") key: String,
@Field("value") value: String,
): Flow<HttpResponse>
}

View File

@@ -1,25 +0,0 @@
/*
* 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.domain.library.service
import de.jensklingenberg.ktorfit.http.DELETE
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Path
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
interface LibraryRepositoryOld {
@GET("api/v1/manga/{mangaId}/library")
fun addMangaToLibrary(
@Path("mangaId") mangaId: Long,
): Flow<HttpResponse>
@DELETE("api/v1/manga/{mangaId}/library")
fun removeMangaFromLibrary(
@Path("mangaId") mangaId: Long,
): Flow<HttpResponse>
}

View File

@@ -1,48 +0,0 @@
/*
* 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.domain.manga.service
import ca.gosyer.jui.domain.manga.model.Manga
import de.jensklingenberg.ktorfit.http.Field
import de.jensklingenberg.ktorfit.http.FormUrlEncoded
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.PATCH
import de.jensklingenberg.ktorfit.http.Path
import de.jensklingenberg.ktorfit.http.Query
import de.jensklingenberg.ktorfit.http.ReqBuilder
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.statement.HttpResponse
import io.ktor.utils.io.ByteReadChannel
import kotlinx.coroutines.flow.Flow
interface MangaRepositoryOld {
@GET("api/v1/manga/{mangaId}/")
fun getManga(
@Path("mangaId") mangaId: Long,
@Query("onlineFetch") refresh: Boolean = false,
): Flow<Manga>
@GET("api/v1/manga/{mangaId}/full")
fun getMangaFull(
@Path("mangaId") mangaId: Long,
@Query("onlineFetch") refresh: Boolean = false,
): Flow<Manga>
@GET("api/v1/manga/{mangaId}/thumbnail")
fun getMangaThumbnail(
@Path("mangaId") mangaId: Long,
@ReqBuilder block: HttpRequestBuilder.() -> Unit,
): Flow<ByteReadChannel>
@PATCH("api/v1/manga/{mangaId}/meta")
@FormUrlEncoded
fun updateMangaMeta(
@Path("mangaId") mangaId: Long,
@Field("key") key: String,
@Field("value") value: String,
): Flow<HttpResponse>
}

View File

@@ -6,7 +6,7 @@
package ca.gosyer.jui.domain.settings.interactor package ca.gosyer.jui.domain.settings.interactor
import ca.gosyer.jui.domain.settings.service.SettingsRepositoryOld import ca.gosyer.jui.domain.settings.service.SettingsRepository
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.singleOrNull import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
@@ -15,7 +15,7 @@ import org.lighthousegames.logging.logging
class AboutServer class AboutServer
@Inject @Inject
constructor( constructor(
private val settingsRepositoryOld: SettingsRepositoryOld, private val settingsRepository: SettingsRepository,
) { ) {
suspend fun await(onError: suspend (Throwable) -> Unit = {}) = suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
asFlow() asFlow()
@@ -25,7 +25,7 @@ class AboutServer
} }
.singleOrNull() .singleOrNull()
fun asFlow() = settingsRepositoryOld.aboutServer() fun asFlow() = settingsRepository.aboutServer()
companion object { companion object {
private val log = logging() private val log = logging()

View File

@@ -1,33 +0,0 @@
/*
* 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.domain.settings.interactor
import ca.gosyer.jui.domain.settings.service.SettingsRepositoryOld
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging
class CheckUpdate
@Inject
constructor(
private val settingsRepositoryOld: SettingsRepositoryOld,
) {
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
asFlow()
.catch {
onError(it)
log.warn(it) { "Failed to check for server updates" }
}
.singleOrNull()
fun asFlow() = settingsRepositoryOld.checkUpdate()
companion object {
private val log = logging()
}
}

View File

@@ -15,7 +15,6 @@ import kotlinx.serialization.Serializable
data class About( data class About(
val name: String, val name: String,
val version: String, val version: String,
val revision: String,
val buildType: AboutBuildType, val buildType: AboutBuildType,
val buildTime: Long, val buildTime: Long,
val github: String, val github: String,

View File

@@ -6,6 +6,7 @@
package ca.gosyer.jui.domain.settings.service package ca.gosyer.jui.domain.settings.service
import ca.gosyer.jui.domain.settings.model.About
import ca.gosyer.jui.domain.settings.model.SetSettingsInput import ca.gosyer.jui.domain.settings.model.SetSettingsInput
import ca.gosyer.jui.domain.settings.model.Settings import ca.gosyer.jui.domain.settings.model.Settings
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -14,4 +15,6 @@ interface SettingsRepository {
fun getSettings(): Flow<Settings> fun getSettings(): Flow<Settings>
fun setSettings(input: SetSettingsInput): Flow<Unit> fun setSettings(input: SetSettingsInput): Flow<Unit>
fun aboutServer(): Flow<About>
} }

View File

@@ -1,21 +0,0 @@
/*
* 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.domain.settings.service
import ca.gosyer.jui.domain.settings.model.About
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.POST
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
interface SettingsRepositoryOld {
@GET("api/v1/settings/about")
fun aboutServer(): Flow<About>
@POST("api/v1/settings/check-update")
fun checkUpdate(): Flow<HttpResponse>
}

View File

@@ -1,92 +0,0 @@
/*
* 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.domain.source.service
import ca.gosyer.jui.domain.source.model.MangaPage
import ca.gosyer.jui.domain.source.model.Source
import ca.gosyer.jui.domain.source.model.sourcefilters.SourceFilterChangeOld
import ca.gosyer.jui.domain.source.model.sourcefilters.SourceFilterData
import ca.gosyer.jui.domain.source.model.sourcefilters.SourceFilterOld
import ca.gosyer.jui.domain.source.model.sourcepreference.SourcePreferenceChange
import ca.gosyer.jui.domain.source.model.sourcepreference.SourcePreferenceOld
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Headers
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Path
import de.jensklingenberg.ktorfit.http.Query
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
interface SourceRepositoryOld {
@GET("api/v1/source/list")
fun getSourceList(): Flow<List<Source>>
@GET("api/v1/source/{sourceId}")
fun getSourceInfo(
@Path("sourceId") sourceId: Long,
): Flow<Source>
@GET("api/v1/source/{sourceId}/popular/{pageNum}")
fun getPopularManga(
@Path("sourceId") sourceId: Long,
@Path("pageNum") pageNum: Int,
): Flow<MangaPage>
@GET("api/v1/source/{sourceId}/latest/{pageNum}")
fun getLatestManga(
@Path("sourceId") sourceId: Long,
@Path("pageNum") pageNum: Int,
): Flow<MangaPage>
@GET("api/v1/source/{sourceId}/search")
fun getSearchResults(
@Path("sourceId") sourceId: Long,
@Query("searchTerm") searchTerm: String?,
@Query("pageNum") pageNum: Int,
): Flow<MangaPage>
@GET("api/v1/source/{sourceId}/filters")
fun getFilterList(
@Path("sourceId") sourceId: Long,
@Query("reset") reset: Boolean = false,
): Flow<List<SourceFilterOld>>
@POST("api/v1/source/{sourceId}/filters")
@Headers("Content-Type: application/json")
fun setFilter(
@Path("sourceId") sourceId: Long,
@Body sourceFilter: SourceFilterChangeOld,
): Flow<HttpResponse>
@POST("api/v1/source/{sourceId}/filters")
@Headers("Content-Type: application/json")
fun setFilters(
@Path("sourceId") sourceId: Long,
@Body sourceFilters: List<SourceFilterChangeOld>,
): Flow<HttpResponse>
@POST("api/v1/source/{sourceId}/quick-search")
@Headers("Content-Type: application/json")
fun getQuickSearchResults(
@Path("sourceId") sourceId: Long,
@Query("pageNum") pageNum: Int,
@Body filterData: SourceFilterData,
): Flow<MangaPage>
@GET("api/v1/source/{sourceId}/preferences")
fun getSourceSettings(
@Path("sourceId") sourceId: Long,
): Flow<List<SourcePreferenceOld>>
@POST("api/v1/source/{sourceId}/preferences")
@Headers("Content-Type: application/json")
fun setSourceSetting(
@Path("sourceId") sourceId: Long,
@Body sourcePreference: SourcePreferenceChange,
): Flow<HttpResponse>
}

View File

@@ -1,32 +0,0 @@
/*
* 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.domain.updates.service
import ca.gosyer.jui.domain.updates.model.Updates
import de.jensklingenberg.ktorfit.http.Field
import de.jensklingenberg.ktorfit.http.FormUrlEncoded
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Path
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
interface UpdatesRepositoryOld {
@GET("api/v1/update/recentChapters/{pageNum}/")
fun getRecentUpdates(
@Path("pageNum") pageNum: Int,
): Flow<Updates>
@POST("api/v1/update/fetch/")
fun updateLibrary(): Flow<HttpResponse>
@POST("api/v1/update/fetch/")
@FormUrlEncoded
fun updateCategory(
@Field("category") categoryId: Long,
): Flow<HttpResponse>
}

View File

@@ -187,7 +187,7 @@ private fun ServerVersionInfo(
} else { } else {
PreferenceRow( PreferenceRow(
title = stringResource(MR.strings.server_version), title = stringResource(MR.strings.server_version),
subtitle = "${about.buildType.name} ${about.version}-${about.revision} ($formattedBuildTime)", subtitle = "${about.buildType.name} ${about.version} ($formattedBuildTime)",
) )
} }
} }

View File

@@ -42,6 +42,7 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -90,7 +91,9 @@ class MangaScreenViewModel
private val loadingManga = MutableStateFlow(true) private val loadingManga = MutableStateFlow(true)
private val loadingChapters = MutableStateFlow(true) private val loadingChapters = MutableStateFlow(true)
val isLoading = combine(loadingManga, loadingChapters) { a, b -> a || b } private val refreshingChapters = MutableStateFlow(false)
private val refreshingManga = MutableStateFlow(false)
val isLoading = combine(loadingManga, loadingChapters, refreshingManga, refreshingChapters) { a, b, c, d -> a || b || c || d }
.stateIn(scope, SharingStarted.Eagerly, true) .stateIn(scope, SharingStarted.Eagerly, true)
val categories = getCategories.asFlow(true) val categories = getCategories.asFlow(true)
@@ -137,19 +140,11 @@ class MangaScreenViewModel
} }
.onEach { .onEach {
_manga.value = it _manga.value = it
if (_manga.value?.initialized == false) {
refreshManga.await(
params.mangaId,
onError = {
log.warn(it) { "Error when fetching manga" }
toast(it.message.orEmpty())
}
)
}
loadingManga.value = false loadingManga.value = false
} }
.catch { .catch {
toast(it.message.orEmpty()) toast(it.message.orEmpty())
log.warn(it) { "Error when loading manga" }
loadingManga.value = false loadingManga.value = false
} }
.launchIn(scope) .launchIn(scope)
@@ -159,16 +154,7 @@ class MangaScreenViewModel
getChapters.asFlow(params.mangaId) getChapters.asFlow(params.mangaId)
} }
.onEach { .onEach {
_chapters.value = it.toDownloadChapters() updateChapters(it)
if (_chapters.value.isEmpty()) {
refreshChapters.await(
params.mangaId,
onError = {
log.warn(it) { "Error when fetching chapters" }
toast(it.message.orEmpty())
}
)
}
loadingChapters.value = false loadingChapters.value = false
} }
.catch { .catch {
@@ -184,6 +170,13 @@ class MangaScreenViewModel
_mangaCategories.value = mangaCategories.toImmutableList() _mangaCategories.value = mangaCategories.toImmutableList()
} }
} }
scope.launch {
val manga = manga.first { it != null }!!
if (!manga.initialized) {
refreshManga()
}
}
} }
fun loadManga() { fun loadManga() {
@@ -198,28 +191,38 @@ class MangaScreenViewModel
} }
} }
fun updateChapters(chapters: List<Chapter>) {
_chapters.value = chapters.sortedByDescending { it.index }.toDownloadChapters()
}
fun refreshManga() { fun refreshManga() {
scope.launch { scope.launch {
loadingManga.value = true refreshingManga.value = true
refreshManga.await( val manga = refreshManga.await(
params.mangaId, params.mangaId,
onError = { onError = {
log.warn(it) { "Error when refreshing manga" } log.warn(it) { "Error when refreshing manga" }
toast(it.message.orEmpty()) toast(it.message.orEmpty())
} }
) )
loadingManga.value = false if (manga != null) {
_manga.value = manga
}
refreshingManga.value = false
} }
scope.launch { scope.launch {
loadingChapters.value = true refreshingChapters.value = true
refreshChapters.await( val chapters = refreshChapters.await(
params.mangaId, params.mangaId,
onError = { onError = {
log.warn(it) { "Error when refreshing chapters" } log.warn(it) { "Error when refreshing chapters" }
toast(it.message.orEmpty()) toast(it.message.orEmpty())
} }
) )
loadingChapters.value = false if (!chapters.isNullOrEmpty()) {
updateChapters(chapters)
}
refreshingChapters.value = false
} }
} }
@@ -242,6 +245,7 @@ class MangaScreenViewModel
chooseCategoriesFlow.emit(Unit) chooseCategoriesFlow.emit(Unit)
} }
} }
loadManga()
} }
} }
} }
@@ -275,6 +279,8 @@ class MangaScreenViewModel
if (mangaCategories != null) { if (mangaCategories != null) {
_mangaCategories.value = mangaCategories.toImmutableList() _mangaCategories.value = mangaCategories.toImmutableList()
} }
loadManga()
} }
} }
} }
@@ -287,6 +293,7 @@ class MangaScreenViewModel
manga.value?.let { manga.value?.let {
updateChapter.await(chapterIds, read = read, onError = { toast(it.message.orEmpty()) }) updateChapter.await(chapterIds, read = read, onError = { toast(it.message.orEmpty()) })
_selectedIds.value = persistentListOf() _selectedIds.value = persistentListOf()
loadChapters()
} }
} }
} }
@@ -303,6 +310,7 @@ class MangaScreenViewModel
manga.value?.let { manga.value?.let {
updateChapter.await(chapterIds, bookmarked = bookmark, onError = { toast(it.message.orEmpty()) }) updateChapter.await(chapterIds, bookmarked = bookmark, onError = { toast(it.message.orEmpty()) })
_selectedIds.value = persistentListOf() _selectedIds.value = persistentListOf()
loadChapters()
} }
} }
} }
@@ -319,6 +327,7 @@ class MangaScreenViewModel
.subList(0, index).map{it.chapter.id} // todo test .subList(0, index).map{it.chapter.id} // todo test
updateChapter.await(chapters, read = true, onError = { toast(it.message.orEmpty()) }) updateChapter.await(chapters, read = true, onError = { toast(it.message.orEmpty()) })
_selectedIds.value = persistentListOf() _selectedIds.value = persistentListOf()
loadChapters()
} }
} }
} }